Skip to content
Merged
17 changes: 10 additions & 7 deletions documentation/content/doc/toolbar.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,19 @@ Rectangle annotations can be tagged with a label. Use the palette in the upper l

### Polygon

With the polygon tool selected, the left mouse button places and adjusts control points.
Before a polygon is finished, right click to remove the last point or press the Esc key to remove all points.
With the polygon tool selected:

After placing 3 points, you can finish the polygon by
- Place points: left mouse button.
- Remove last placed point: right mouse button.
- Remove all points: `Esc` key.
- Close a polygon after placing 3 points: Click first point, press `Enter` key or double click left mouse.

- Clicking the first control point.
- Press the `Enter` key
- Double clicking the left mouse button.
After closing a polygon:

After a polygon is closed, right click a control point to delete the polygon.
- Move point: Drag point with left mouse button.
- Add point: Left mouse button on polygon line.
- Delete point: right click point and select Delete Point.
- Delete polygon: right click point or line and select Delete Polygon.

Polygon annotations can be tagged with a label. Use the palette in the upper left or the `q` or `w` keys to select the active label.

Expand Down
21 changes: 16 additions & 5 deletions src/components/tools/polygon/PolygonTool.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,15 @@
>
<v-list density="compact">
<v-list-item @click="deleteToolFromContextMenu">
<v-list-item-title>Delete</v-list-item-title>
<v-list-item-title>Delete Polygon</v-list-item-title>
</v-list-item>
<!-- Optional items below stable item for muscle memory -->
<v-list-item
v-for="action in contextMenu.widgetActions"
@click="action.func"
:key="action.name"
>
<v-list-item-title>{{ action.name }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
Expand All @@ -51,14 +59,15 @@ import { useToolStore } from '@/src/store/tools';
import { Tools } from '@/src/store/tools/types';
import { getLPSAxisFromDir } from '@/src/utils/lps';
import vtkWidgetManager from '@kitware/vtk.js/Widgets/Core/WidgetManager';
import type { Vector2, Vector3 } from '@kitware/vtk.js/types';
import type { Vector3 } from '@kitware/vtk.js/types';
import { LPSAxisDir } from '@/src/types/lps';
import {
FrameOfReference,
frameOfReferenceToImageSliceAndAxis,
} from '@/src/utils/frameOfReference';
import { usePolygonStore } from '@/src/store/tools/polygons';
import { Polygon, PolygonID } from '@/src/types/polygon';
import { ContextMenuEvent, Polygon, PolygonID } from '@/src/types/polygon';
import { WidgetAction } from '@/src/vtk/ToolWidgetUtils/utils';
import PolygonWidget2D from './PolygonWidget2D.vue';

type ToolID = PolygonID;
Expand Down Expand Up @@ -198,12 +207,14 @@ export default defineComponent({
x: 0,
y: 0,
forToolID: '' as ToolID,
widgetActions: [] as Array<WidgetAction>,
});

const openContextMenu = (toolID: ToolID, displayXY: Vector2) => {
[contextMenu.x, contextMenu.y] = displayXY;
const openContextMenu = (toolID: ToolID, event: ContextMenuEvent) => {
[contextMenu.x, contextMenu.y] = event.displayXY;
contextMenu.show = true;
contextMenu.forToolID = toolID;
contextMenu.widgetActions = event.widgetActions;
};

const deleteToolFromContextMenu = () => {
Expand Down
11 changes: 7 additions & 4 deletions src/components/tools/polygon/PolygonWidget2D.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { getCSSCoordinatesFromEvent } from '@/src/utils/vtk-helpers';
import { LPSAxisDir } from '@/src/types/lps';
import { useVTKCallback } from '@/src/composables/useVTKCallback';
import { usePolygonStore as useStore } from '@/src/store/tools/polygons';
import { PolygonID as ToolID } from '@/src/types/polygon';
import { ContextMenuEvent, PolygonID as ToolID } from '@/src/types/polygon';
import vtkWidgetFactory, {
vtkPolygonViewWidget as WidgetView,
} from '@/src/vtk/PolygonWidget';
Expand Down Expand Up @@ -118,9 +118,12 @@ export default defineComponent({
return;
}
rightClickSub = widget.value.onRightClickEvent((eventData) => {
const coords = getCSSCoordinatesFromEvent(eventData);
if (coords) {
emit('contextmenu', coords);
const displayXY = getCSSCoordinatesFromEvent(eventData);
if (displayXY) {
emit('contextmenu', {
displayXY,
widgetActions: eventData.widgetActions,
} satisfies ContextMenuEvent);
}
});
});
Expand Down
8 changes: 7 additions & 1 deletion src/types/polygon.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Vector3 } from '@kitware/vtk.js/types';
import type { Vector2, Vector3 } from '@kitware/vtk.js/types';
import { AnnotationTool } from './annotation-tool';
import { WidgetAction } from '../vtk/ToolWidgetUtils/utils';

export type PolygonID = string & { __type: 'PolygonID' };

Expand All @@ -10,3 +11,8 @@ export type Polygon = {
points: Array<Vector3>;
movePoint: Vector3;
} & AnnotationTool<PolygonID>;

export type ContextMenuEvent = {
displayXY: Vector2;
widgetActions: Array<WidgetAction>;
};
149 changes: 149 additions & 0 deletions src/vtk/LineGlyphRepresentation/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import macro from '@kitware/vtk.js/macros';
import * as vtkMath from '@kitware/vtk.js/Common/Core/Math';
import vtkGlyph3DMapper from '@kitware/vtk.js/Rendering/Core/Glyph3DMapper';
import vtkGlyphRepresentation from '@kitware/vtk.js/Widgets/Representations/GlyphRepresentation';
import { Behavior } from '@kitware/vtk.js/Widgets/Representations/WidgetRepresentation/Constants';
import { allocateArray } from '@kitware/vtk.js/Widgets/Representations/WidgetRepresentation';
import vtkCylinderSource from '@kitware/vtk.js/Filters/Sources/CylinderSource';
import { OrientationModes } from '@kitware/vtk.js/Rendering/Core/Glyph3DMapper/Constants';
import { getPixelWorldHeightAtCoord } from '@kitware/vtk.js/Widgets/Core/WidgetManager';

function vtkLineGlyphRepresentation(publicAPI, model) {
model.classHierarchy.push('vtkLineGlyphRepresentation');

publicAPI.setGlyphResolution = macro.chain(
publicAPI.setGlyphResolution,
model._pipeline.glyph.setResolution
);
}

function cylinderScale(publicAPI, model) {
return (polyData, states) => {
model._pipeline.mapper.setScaleArray('scale');
model._pipeline.mapper.setScaleFactor(1);
model._pipeline.mapper.setScaling(true);
model._pipeline.mapper.setScaleMode(
vtkGlyph3DMapper.ScaleModes.SCALE_BY_COMPONENTS
);
const scales = allocateArray(
polyData,
'scale',
states.length,
'Float32Array',
3
).getData();
let j = 0;
for (let i = 0; i < states.length; ++i) {
const state = states[i];
const origin = state.getOrigin();
const nextOrigin =
states[i === states.length - 1 ? 0 : i + 1].getOrigin();

const direction = vtkMath.subtract(nextOrigin, origin, []);
const length = vtkMath.normalize(direction);

let scaleFactor = state.getActive() ? model.activeScaleFactor : 1;
if (publicAPI.getScaleInPixels()) {
scaleFactor *= getPixelWorldHeightAtCoord(
state.getOrigin(),
model.displayScaleParams
);
}
if (!model.forceLineThickness) {
scaleFactor *= state.getScale1?.() ?? 1;
}
const scale = [1, model.lineThickness, model.lineThickness];
scales[j++] = length * scale[0];
scales[j++] = scaleFactor * scale[1];
scales[j++] = scaleFactor * scale[2];
}
};
}

function cylinderDirection(publicAPI, model) {
return (polyData, states) => {
model._pipeline.mapper.setOrientationArray('orientation');
model._pipeline.mapper.setOrientationMode(OrientationModes.MATRIX);
const orientation = allocateArray(
polyData,
'orientation',
states.length,
'Float32Array',
9
).getData();
for (let i = 0; i < states.length; ++i) {
const state = states[i];
const origin = state.getOrigin();
const nextOrigin =
states[i === states.length - 1 ? 0 : i + 1].getOrigin();

const direction = vtkMath.subtract(nextOrigin, origin, []);
vtkMath.normalize(direction);
const right = [1, 0, 0];
const up = [0, 1, 0];
vtkMath.perpendiculars(direction, up, right, 0);

orientation.set(direction, 9 * i);
orientation.set(up, 9 * i + 3);
orientation.set(right, 9 * i + 6);
}
};
}

// ----------------------------------------------------------------------------
// Object factory
// ----------------------------------------------------------------------------

function defaultValues(publicAPI, model, initialValues) {
return {
behavior: Behavior.CONTEXT,
glyphResolution: 32,
lineThickness: 0.5, // radius of the cylinder
forceLineThickness: false,
...initialValues,
_pipeline: {
glyph:
initialValues?.pipeline?.glyph ??
vtkCylinderSource.newInstance({
direction: [1, 0, 0],
center: [0.5, 0, 0], // origin of cylinder at end, not center
capping: false,
}),
...initialValues?.pipeline,
},
applyMixin: {
noScale: cylinderScale(publicAPI, model),
scale1: cylinderScale(publicAPI, model),
noOrientation: cylinderDirection(publicAPI, model),
},
};
}

// ----------------------------------------------------------------------------

export function extend(publicAPI, model, initialValues = {}) {
vtkGlyphRepresentation.extend(
publicAPI,
model,
defaultValues(publicAPI, model, initialValues)
);
macro.setGet(publicAPI, model, [
'glyphResolution',
'lineThickness',
'forceLineThickness',
]);
macro.get(publicAPI, model._pipeline, ['glyph', 'mapper', 'actor']);

vtkLineGlyphRepresentation(publicAPI, model);
}

// ----------------------------------------------------------------------------

export const newInstance = macro.newInstance(
extend,
'vtkLineGlyphRepresentation'
);

// ----------------------------------------------------------------------------

export default { newInstance, extend };
Loading