ImageCroppingWidget

Source

behavior.js
import macro from 'vtk.js/Sources/macros';
import { add } from 'vtk.js/Sources/Common/Core/Math';

import {
AXES,
transformVec3,
handleTypeFromName,
calculateDirection,
calculateCropperCenter,
} from 'vtk.js/Sources/Widgets/Widgets3D/ImageCroppingWidget/helpers';

export default function widgetBehavior(publicAPI, model) {
model._isDragging = false;

publicAPI.setDisplayCallback = (callback) =>
model.representations[0].setDisplayCallback(callback);

publicAPI.handleLeftButtonPress = (callData) => {
if (
!model.activeState ||
!model.activeState.getActive() ||
!model.pickable
) {
return macro.VOID;
}
if (model.dragable) {
// updates worldDelta
model.activeState
.getManipulator()
.handleEvent(callData, model._apiSpecificRenderWindow);
model._isDragging = true;
model._apiSpecificRenderWindow.setCursor('grabbing');
model._interactor.requestAnimation(publicAPI);
}
return macro.EVENT_ABORT;
};

publicAPI.handleMouseMove = (callData) => {
if (model._isDragging) {
return publicAPI.handleEvent(callData);
}
return macro.VOID;
};

publicAPI.handleLeftButtonRelease = () => {
if (
!model.activeState ||
!model.activeState.getActive() ||
!model.pickable
) {
return macro.VOID;
}

if (model._isDragging) {
model._isDragging = false;
model._interactor.cancelAnimation(publicAPI);
model.widgetState.deactivate();
}

return macro.EVENT_ABORT;
};

publicAPI.handleEvent = (callData) => {
if (model.pickable && model.activeState && model.activeState.getActive()) {
const manipulator = model.activeState.getManipulator();
if (manipulator) {
const name = model.activeState.getName();
const type = handleTypeFromName(name);
const index = name.split('').map((l) => AXES.indexOf(l));
const planes = model.widgetState.getCroppingPlanes().getPlanes();
const indexToWorldT = model.widgetState.getIndexToWorldT();

let worldCoords = [];
let worldDelta = [];

if (type === 'corners') {
// manipulator should be a plane manipulator
({ worldCoords, worldDelta } = manipulator.handleEvent(
callData,
model._apiSpecificRenderWindow
));
}

if (type === 'faces') {
// get center of current crop box
const worldCenter = calculateCropperCenter(planes, indexToWorldT);

manipulator.setHandleOrigin(worldCenter);
manipulator.setHandleNormal(
calculateDirection(model.activeState.getOrigin(), worldCenter)
);
({ worldCoords, worldDelta } = manipulator.handleEvent(
callData,
model._apiSpecificRenderWindow
));
}

if (type === 'edges') {
// constrain to a plane with a normal parallel to the edge
const edgeAxis = index.map((a) => (a === 1 ? a : 0));
const faceName = edgeAxis.map((i) => AXES[i + 1]).join('');
const handle = model.widgetState.getStatesWithLabel(faceName)[0];
// get center of current crop box
const worldCenter = calculateCropperCenter(planes, indexToWorldT);

manipulator.setHandleNormal(
calculateDirection(handle.getOrigin(), worldCenter)
);
({ worldCoords, worldDelta } = manipulator.handleEvent(
callData,
model._apiSpecificRenderWindow
));
}

if (worldCoords.length && worldDelta.length) {
// transform worldCoords to indexCoords, and then update the croppingPlanes() state with setPlanes().
const worldToIndexT = model.widgetState.getWorldToIndexT();
const indexCoords = transformVec3(worldCoords, worldToIndexT);

for (let i = 0; i < 3; i++) {
if (index[i] === 0) {
planes[i * 2] = indexCoords[i];
} else if (index[i] === 2) {
planes[i * 2 + 1] = indexCoords[i];
}
}

model.activeState.setOrigin(
add(model.activeState.getOrigin(), worldDelta, [])
);
model.widgetState.getCroppingPlanes().setPlanes(...planes);

return macro.EVENT_ABORT;
}
}
}
return macro.VOID;
};

// --------------------------------------------------------------------------
// initialization
// --------------------------------------------------------------------------

model._camera = model._renderer.getActiveCamera();

model.classHierarchy.push('vtkImageCroppingWidgetProp');
}
helpers.js
import { quat, mat4, vec3 } from 'gl-matrix';

// Labels used to encode handle position in the handle state's name property
export const AXES = ['-', '=', '+'];

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

export function transformVec3(ain, transform) {
const vout = new Float64Array(3);
vec3.transformMat4(vout, ain, transform);
return vout;
}

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

export function rotateVec3(vec, transform) {
// transform is a mat4
const out = vec3.create();
const q = quat.create();
mat4.getRotation(q, transform);
vec3.transformQuat(out, vec, q);
return out;
}

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

export function handleTypeFromName(name) {
const [i, j, k] = name.split('').map((l) => AXES.indexOf(l) - 1);
if (i * j * k !== 0) {
return 'corners';
}
if (i * j !== 0 || j * k !== 0 || k * i !== 0) {
return 'edges';
}
return 'faces';
}

export function calculateCropperCenter(planes, transform) {
// get center of current crop box
const center = [
(planes[0] + planes[1]) / 2,
(planes[2] + planes[3]) / 2,
(planes[4] + planes[5]) / 2,
];
return transformVec3(center, transform);
}

export function calculateDirection(v1, v2) {
const direction = vec3.create();
vec3.subtract(direction, v1, v2);
vec3.normalize(direction, direction);
return direction;
}
index.js
import macro from 'vtk.js/Sources/macros';
import vtkAbstractWidgetFactory from 'vtk.js/Sources/Widgets/Core/AbstractWidgetFactory';
import vtkPlaneManipulator from 'vtk.js/Sources/Widgets/Manipulators/PlaneManipulator';
import vtkLineManipulator from 'vtk.js/Sources/Widgets/Manipulators/LineManipulator';
import vtkSphereHandleRepresentation from 'vtk.js/Sources/Widgets/Representations/SphereHandleRepresentation';
import vtkCroppingOutlineRepresentation from 'vtk.js/Sources/Widgets/Representations/CroppingOutlineRepresentation';

import behavior from 'vtk.js/Sources/Widgets/Widgets3D/ImageCroppingWidget/behavior';
import state from 'vtk.js/Sources/Widgets/Widgets3D/ImageCroppingWidget/state';

import {
AXES,
transformVec3,
} from 'vtk.js/Sources/Widgets/Widgets3D/ImageCroppingWidget/helpers';

import { ViewTypes } from 'vtk.js/Sources/Widgets/Core/WidgetManager/Constants';

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

// ----------------------------------------------------------------------------
// Factory
// ----------------------------------------------------------------------------

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

const superClass = { ...publicAPI };

let stateSub = null;

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

function setHandlesEnabled(label, flag) {
model.widgetState.getStatesWithLabel(label).forEach((handle) => {
handle.setVisible(flag);
});
}

// Set the visibility of the three classes of handles: face, edge, corner
publicAPI.setFaceHandlesEnabled = (flag) => setHandlesEnabled('faces', flag);
publicAPI.setEdgeHandlesEnabled = (flag) => setHandlesEnabled('edges', flag);
publicAPI.setCornerHandlesEnabled = (flag) =>
setHandlesEnabled('corners', flag);

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

// Copies the transforms and dimension of a vtkImageData
publicAPI.copyImageDataDescription = (im) => {
model.widgetState.setIndexToWorldT(...im.getIndexToWorld());
model.widgetState.setWorldToIndexT(...im.getWorldToIndex());

const dims = im.getDimensions();
const planeState = model.widgetState.getCroppingPlanes();
planeState.setPlanes([0, dims[0], 0, dims[1], 0, dims[2]]);

publicAPI.modified();
};

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

// Updates handle positions based on cropping planes
publicAPI.updateHandles = () => {
const planes = model.widgetState.getCroppingPlanes().getPlanes();
const midpts = [
(planes[0] + planes[1]) / 2,
(planes[2] + planes[3]) / 2,
(planes[4] + planes[5]) / 2,
];
const iAxis = [planes[0], midpts[0], planes[1]];
const jAxis = [planes[2], midpts[1], planes[3]];
const kAxis = [planes[4], midpts[2], planes[5]];

const indexToWorldT = model.widgetState.getIndexToWorldT();
const getAxis = (a) => AXES[a];
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
for (let k = 0; k < 3; k++) {
// skip center of box
if (i !== 1 || j !== 1 || k !== 1) {
const name = [i, j, k].map(getAxis).join('');
const coord = transformVec3(
[iAxis[i], jAxis[j], kAxis[k]],
indexToWorldT
);

const [handle] = model.widgetState.getStatesWithLabel(name);
handle.setOrigin(...coord);
}
}
}
}
};

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

publicAPI.delete = macro.chain(publicAPI.delete, () => {
if (stateSub) {
stateSub.unsubscribe();
}
});

// --- Widget Requirement ---------------------------------------------------

model.methodsToLink = ['scaleInPixels'];

// Given a view type (geometry, slice, volume), return a description
// of what representations to create and what widget state to pass
// to the respective representations.
publicAPI.getRepresentationsForViewType = (viewType) => {
switch (viewType) {
case ViewTypes.DEFAULT:
case ViewTypes.GEOMETRY:
case ViewTypes.SLICE:
case ViewTypes.VOLUME:
default:
return [
// Describes constructing a vtkSphereHandleRepresentation, and every
// time the widget state updates, we will give the representation
// a list of all handle states (which have the label "handles").
{ builder: vtkSphereHandleRepresentation, labels: ['handles'] },
{
builder: vtkCroppingOutlineRepresentation,
// outline is defined by corner points
labels: ['corners'],
},
];
}
};

// Update handle positions when cropping planes update
stateSub = model.widgetState
.getCroppingPlanes()
.onModified(publicAPI.updateHandles);

publicAPI.setCornerManipulator = (manipulator) => {
superClass.setCornerManipulator(manipulator);
model.widgetState
.getStatesWithLabel('corners')
.forEach((handle) => handle.setManipulator(manipulator));
};

publicAPI.setEdgeManipulator = (manipulator) => {
superClass.setEdgeManipulator(manipulator);
model.widgetState
.getStatesWithLabel('edges')
.forEach((handle) => handle.setManipulator(manipulator));
};

publicAPI.setFaceManipulator = (manipulator) => {
superClass.setFaceManipulator(manipulator);
model.widgetState
.getStatesWithLabel('faces')
.forEach((handle) => handle.setManipulator(manipulator));
};

// --------------------------------------------------------------------------
// initialization
// --------------------------------------------------------------------------

publicAPI.setCornerManipulator(
vtkPlaneManipulator.newInstance({ useCameraNormal: true })
);
publicAPI.setEdgeManipulator(vtkPlaneManipulator.newInstance());
publicAPI.setFaceManipulator(vtkLineManipulator.newInstance());
}

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

const defaultValues = (initialValues) => ({
// cornerManipulator: null,
// edgeManipulator: null,
// faceManipulator: null,
behavior,
widgetState: state(),
...initialValues,
});

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

export function extend(publicAPI, model, initialValues = {}) {
Object.assign(model, defaultValues(initialValues));

vtkAbstractWidgetFactory.extend(publicAPI, model, initialValues);
macro.setGet(publicAPI, model, [
'cornerManipulator',
'edgeManipulator',
'faceManipulator',
]);

vtkImageCroppingWidget(publicAPI, model);
}

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

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

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

export default { newInstance, extend };
state.js
import vtkStateBuilder from 'vtk.js/Sources/Widgets/Core/StateBuilder';

import {
AXES,
handleTypeFromName,
} from 'vtk.js/Sources/Widgets/Widgets3D/ImageCroppingWidget/helpers';

export default function build() {
// create our state builder
const builder = vtkStateBuilder.createBuilder();

// add image data description fields
builder
.addField({
name: 'indexToWorldT',
initialValue: Array(16).fill(0),
})
.addField({
name: 'worldToIndexT',
initialValue: Array(16).fill(0),
});

// make cropping planes a sub-state so we can listen to it
// separately from the rest of the widget state.
const croppingState = vtkStateBuilder
.createBuilder()
.addField({
name: 'planes',
// index space
initialValue: [0, 1, 0, 1, 0, 1],
})
.build();

// add cropping planes state to our primary state
builder.addStateFromInstance({
labels: ['croppingPlanes'],
name: 'croppingPlanes',
instance: croppingState,
});

// add all handle states
// default bounds is [-1, 1] in all dimensions
for (let i = -1; i < 2; i++) {
for (let j = -1; j < 2; j++) {
for (let k = -1; k < 2; k++) {
// skip center of box
if (i !== 0 || j !== 0 || k !== 0) {
const name = AXES[i + 1] + AXES[j + 1] + AXES[k + 1];
const type = handleTypeFromName(name);

// since handle states are rendered via vtkSphereHandleRepresentation,
// we can dictate the handle origin, size (scale1), color, and visibility.
builder.addStateFromMixin({
labels: ['handles', name, type],
mixins: [
'name',
'origin',
'color',
'scale1',
'visible',
'manipulator',
],
name,
initialValues: {
scale1: 30,
origin: [i, j, k],
visible: true,
name,
},
});
}
}
}
}

return builder.build();
}