Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Tools): add Eraser Tool #806

Merged
merged 19 commits into from
Feb 15, 2024
13 changes: 13 additions & 0 deletions common/reviews/api/tools.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2023,6 +2023,19 @@ declare namespace Enums_2 {
}
}

// @public (undocumented)
export class EraserTool extends BaseTool {
constructor(toolProps?: PublicToolProps, defaultToolProps?: ToolProps);
// (undocumented)
_deleteNearbyAnnotations(evt: EventTypes_2.InteractionEventType, interactionType: string): boolean;
// (undocumented)
preMouseDownCallback: (evt: EventTypes_2.InteractionEventType) => boolean;
// (undocumented)
preTouchStartCallback: (evt: EventTypes_2.InteractionEventType) => boolean;
// (undocumented)
static toolName: any;
}

// @public (undocumented)
enum Events {
// (undocumented)
Expand Down
5 changes: 5 additions & 0 deletions packages/tools/examples/stackAnnotationTools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const {
ToolGroupManager,
ArrowAnnotateTool,
PlanarFreehandROITool,
EraserTool,
KeyImageTool,
Enums: csToolsEnums,
} = cornerstoneTools;
Expand Down Expand Up @@ -119,6 +120,7 @@ const toolsNames = [
CobbAngleTool.toolName,
ArrowAnnotateTool.toolName,
PlanarFreehandROITool.toolName,
EraserTool.toolName,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding the eraser tool to the example. I tried it and was able to delete every annotation type except the probe. Could you please have a look at why this might be? Thanks.

KeyImageTool.toolName,
];
let selectedToolName = toolsNames[0];
Expand Down Expand Up @@ -218,6 +220,7 @@ async function run() {
cornerstoneTools.addTool(CobbAngleTool);
cornerstoneTools.addTool(ArrowAnnotateTool);
cornerstoneTools.addTool(PlanarFreehandROITool);
cornerstoneTools.addTool(EraserTool);
cornerstoneTools.addTool(KeyImageTool);

// Define a tool group, which defines how mouse events map to tool commands for
Expand All @@ -235,6 +238,7 @@ async function run() {
toolGroup.addTool(CobbAngleTool.toolName);
toolGroup.addTool(ArrowAnnotateTool.toolName);
toolGroup.addTool(PlanarFreehandROITool.toolName);
toolGroup.addTool(EraserTool.toolName);
toolGroup.addTool(KeyImageTool.toolName);

// Set the initial state of the tools, here we set one tool active on left click.
Expand All @@ -256,6 +260,7 @@ async function run() {
toolGroup.setToolPassive(AngleTool.toolName);
toolGroup.setToolPassive(ArrowAnnotateTool.toolName);
toolGroup.setToolPassive(PlanarFreehandROITool.toolName);
toolGroup.setToolPassive(EraserTool.toolName);

toolGroup.setToolConfiguration(PlanarFreehandROITool.toolName, {
calculateStats: true,
Expand Down
2 changes: 2 additions & 0 deletions packages/tools/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import {
OrientationMarkerTool,
OverlayGridTool,
SegmentationIntersectionTool,
EraserTool,
SegmentSelectTool,
} from './tools';

Expand Down Expand Up @@ -126,6 +127,7 @@ export {
ReferenceCursors,
ReferenceLines,
ScaleOverlayTool,
EraserTool,
// Segmentation Display
SegmentationDisplayTool,
// Segmentation Editing Tools
Expand Down
96 changes: 96 additions & 0 deletions packages/tools/src/tools/AnnotationEraserTool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { BaseTool } from './base';
import { EventTypes, PublicToolProps, ToolProps } from '../types';
import { ToolGroupManager } from '../store';
import {
getAnnotations,
removeAnnotation,
} from '../stateManagement/annotation/annotationState';
import { setAnnotationSelected } from '../stateManagement/annotation/annotationSelection';

class AnnotationEraserTool extends BaseTool {
static toolName;
constructor(
toolProps: PublicToolProps = {},
defaultToolProps: ToolProps = {
supportedInteractionTypes: ['Mouse', 'Touch'],
}
) {
super(toolProps, defaultToolProps);
}
preMouseDownCallback = (evt: EventTypes.InteractionEventType): boolean => {
return this._deleteNearbyAnnotations(evt, 'mouse');
};
preTouchStartCallback = (evt: EventTypes.InteractionEventType): boolean => {
return this._deleteNearbyAnnotations(evt, 'touch');
};

_deleteNearbyAnnotations(
evt: EventTypes.InteractionEventType,
interactionType: string
): boolean {
const { renderingEngineId, viewportId, element, currentPoints } =
evt.detail;

const toolGroup = ToolGroupManager.getToolGroupForViewport(
viewportId,
renderingEngineId
);

if (!toolGroup) {
return false;
}

const tools = toolGroup._toolInstances;
const annotationsToRemove = [];

for (const toolName in tools) {
const toolInstance = tools[toolName];

if (
typeof toolInstance.isPointNearTool !== 'function' ||
typeof toolInstance.filterInteractableAnnotationsForElement !==
'function'
) {
continue;
}

const annotations = getAnnotations(toolName, element);

if (!annotations) {
continue;
}

const interactableAnnotations =
toolInstance.filterInteractableAnnotationsForElement(
element,
annotations
);

for (const annotation of interactableAnnotations) {
if (
toolInstance.isPointNearTool(
element,
annotation,
currentPoints.canvas,
10,
interactionType
)
) {
annotationsToRemove.push(annotation.annotationUID);
}
}
}

for (const annotationUID of annotationsToRemove) {
setAnnotationSelected(annotationUID);
removeAnnotation(annotationUID);
}

evt.preventDefault();

return true;
}
}

AnnotationEraserTool.toolName = 'Eraser';
export default AnnotationEraserTool;
2 changes: 2 additions & 0 deletions packages/tools/src/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import AngleTool from './annotation/AngleTool';
import CobbAngleTool from './annotation/CobbAngleTool';
import UltrasoundDirectionalTool from './annotation/UltrasoundDirectionalTool';
import KeyImageTool from './annotation/KeyImageTool';
import AnnotationEraserTool from './AnnotationEraserTool';

// Segmentation DisplayTool
import SegmentationDisplayTool from './displayTools/SegmentationDisplayTool';
Expand Down Expand Up @@ -90,6 +91,7 @@ export {
CobbAngleTool,
UltrasoundDirectionalTool,
KeyImageTool,
AnnotationEraserTool as EraserTool,
// Segmentations Display
SegmentationDisplayTool,
// Segmentations Tools
Expand Down
209 changes: 209 additions & 0 deletions packages/tools/test/EraserTool_test.js
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only two changes I needed to have the test pass are suggested below. Please give them a try.

Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import * as cornerstone3D from '@cornerstonejs/core';
import * as csTools3d from '../src/index';
import * as testUtils from '../../../utils/test/testUtils';
import { EraserTool } from '@cornerstonejs/tools';
import { triggerAnnotationAddedForElement } from '../src/stateManagement/annotation/helpers/state';

const {
cache,
RenderingEngine,
Enums,
eventTarget,
utilities,
imageLoader,
metaData,
volumeLoader,
} = cornerstone3D;

const { Events, ViewportType } = Enums;

const {
LengthTool,
ToolGroupManager,
Enums: csToolsEnums,
annotation,
} = csTools3d;

const { Events: csToolsEvents } = csToolsEnums;

const {
fakeImageLoader,
fakeVolumeLoader,
fakeMetaDataProvider,
createNormalizedMouseEvent,
} = testUtils;

const renderingEngineId = utilities.uuidv4();

const viewportId = 'VIEWPORT';

function createViewport(renderingEngine, viewportType, width, height) {
const element = document.createElement('div');

element.style.width = `${width}px`;
element.style.height = `${height}px`;
document.body.appendChild(element);

renderingEngine.setViewports([
{
viewportId: viewportId,
type: viewportType,
element,
defaultOptions: {
background: [1, 0, 1], // pinkish background
orientation: Enums.OrientationAxis.AXIAL,
},
},
]);
return element;
}

const volumeId = `fakeVolumeLoader:volumeURI_100_100_10_1_1_1_0`;

describe('EraserTool:', () => {
beforeAll(() => {
cornerstone3D.setUseCPURendering(false);
});

describe('Cornerstone Tools: -- Eraser', () => {
beforeEach(function () {
csTools3d.init();
csTools3d.addTool(EraserTool);
csTools3d.addTool(LengthTool);

cache.purgeCache();
this.DOMElements = [];

this.stackToolGroup = ToolGroupManager.createToolGroup('stack');
this.stackToolGroup.addTool(LengthTool.toolName, {});
this.stackToolGroup.setToolEnabled(LengthTool.toolName, {});
this.stackToolGroup.addTool(EraserTool.toolName, {});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to add the length tool to the tool group for the stack viewport so that it is picked up in the EraserTool code. So below this line add...

this.stackToolGroup.addTool(LengthTool.toolName, {});

this.stackToolGroup.setToolActive(EraserTool.toolName, {
bindings: [{ mouseButton: 1 }],
});

this.renderingEngine = new RenderingEngine(renderingEngineId);
imageLoader.registerImageLoader('fakeImageLoader', fakeImageLoader);
volumeLoader.registerVolumeLoader('fakeVolumeLoader', fakeVolumeLoader);
metaData.addProvider(fakeMetaDataProvider, 10000);
});

afterEach(function () {
csTools3d.destroy();
eventTarget.reset();
cache.purgeCache();

this.renderingEngine.destroy();
metaData.removeProvider(fakeMetaDataProvider);
imageLoader.unregisterAllImageLoaders();
ToolGroupManager.destroyToolGroup('stack');

this.DOMElements.forEach((el) => {
if (el.parentNode) {
el.parentNode.removeChild(el);
}
});
});

it('Should successfully delete a length annotation on a canvas with mouse down - 512 x 128', function (done) {
const element = createViewport(
this.renderingEngine,
ViewportType.STACK,
512,
128
);

this.DOMElements.push(element);

const imageId1 = 'fakeImageLoader:imageURI_64_64_10_5_1_1_0';
const vp = this.renderingEngine.getViewport(viewportId);

eventTarget.addEventListener(csToolsEvents.ANNOTATION_REMOVED, () => {
const lengthAnnotations = annotation.state.getAnnotations(
LengthTool.toolName,
element
);
expect(lengthAnnotations).toBeDefined();
expect(lengthAnnotations.length).toBe(0);
done();
});

element.addEventListener(Events.IMAGE_RENDERED, () => {
const index1 = [32, 32, 0];
const index2 = [10, 1, 0];

const { imageData } = vp.getImageData();

const {
pageX: pageX1,
pageY: pageY1,
clientX: clientX1,
clientY: clientY1,
worldCoord: worldCoord1,
} = createNormalizedMouseEvent(imageData, index1, element, vp);
const { worldCoord: worldCoord2 } = createNormalizedMouseEvent(
imageData,
index2,
element,
vp
);

const camera = vp.getCamera();
const { viewPlaneNormal, viewUp } = camera;

const lengthAnnotation = {
highlighted: true,
invalidated: true,
metadata: {
toolName: LengthTool.toolName,
viewPlaneNormal: [...viewPlaneNormal],
viewUp: [...viewUp],
FrameOfReferenceUID: vp.getFrameOfReferenceUID(),
referencedImageId: imageId1,
},
data: {
handles: {
points: [[...worldCoord1], [...worldCoord2]],
activeHandleIndex: null,
textBox: {
hasMoved: false,
worldPosition: [0, 0, 0],
worldBoundingBox: {
topLeft: [0, 0, 0],
topRight: [0, 0, 0],
bottomLeft: [0, 0, 0],
bottomRight: [0, 0, 0],
},
},
},
label: '',
cachedStats: {},
},
};

annotation.state.addAnnotation(lengthAnnotation, element);
triggerAnnotationAddedForElement(lengthAnnotation, element);

let evt = new MouseEvent('mousedown', {
target: element,
buttons: 1,
clientX: clientX1,
clientY: clientY1,
pageX: pageX1,
pageY: pageY1,
});

element.dispatchEvent(evt);
});

this.stackToolGroup.addViewport(vp.id, this.renderingEngine.id);

try {
vp.setStack([imageId1], 0);
this.renderingEngine.render();
} catch (e) {
done.fail(e);
}
});
});
});