TransformControlsWidget

Source

behavior.js
import { quat, vec3 } from 'gl-matrix';
import macro from 'vtk.js/Sources/macros';
import vtkBoundingBox from 'vtk.js/Sources/Common/DataModel/BoundingBox';
import vtkPlaneManipulator from 'vtk.js/Sources/Widgets/Manipulators/PlaneManipulator';
import vtkLineManipulator from 'vtk.js/Sources/Widgets/Manipulators/LineManipulator';

export default function widgetBehavior(publicAPI, model) {
let isDragging = false;

model.rotationManipulator = vtkPlaneManipulator.newInstance();
model.lineManipulator = vtkLineManipulator.newInstance();

const rotateState = {
startQuat: quat.create(),
dragStartVec: [0, 0, 0],
};
const scaleState = {
startDistFromOrigin: 0,
startScale: 1,
};
const translateState = {
startPos: 0,
dragStartCoord: [0, 0, 0],
};

publicAPI.getBounds = () => [...vtkBoundingBox.INIT_BOUNDS];

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

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

const [type, axis] = model.activeState.getName().split(':');
const axisIndex = 'XYZ'.indexOf(axis);
if (type === 'translate') {
publicAPI.handleTranslateStartEvent(callData, axis, axisIndex);
} else if (type === 'scale') {
publicAPI.handleScaleStartEvent(callData, axis, axisIndex);
} else if (type === 'rotate') {
publicAPI.handleRotateStartEvent(callData, axis, axisIndex);
}

model._interactor.requestAnimation(publicAPI);
return macro.EVENT_ABORT;
};

publicAPI.handleTranslateStartEvent = (callData, axis, axisIndex) => {
model.lineManipulator.setHandleOrigin(model.activeState.getOrigin());
model.lineManipulator.setHandleNormal(model.activeState.getDirection());

const { worldCoords } = model.lineManipulator.handleEvent(
callData,
model._apiSpecificRenderWindow
);

if (worldCoords.length) {
isDragging = true;
translateState.dragStartCoord = worldCoords;
translateState.startPos = model.widgetState
.getTransform()
.getTranslation()[axisIndex];
}
};

publicAPI.handleScaleStartEvent = (callData, axis, axisIndex) => {
model.lineManipulator.setHandleOrigin(model.activeState.getOrigin());
model.lineManipulator.setHandleNormal(model.activeState.getDirection());

const { worldCoords } = model.lineManipulator.handleEvent(
callData,
model._apiSpecificRenderWindow
);

if (worldCoords.length) {
isDragging = true;
scaleState.startScale = model.widgetState.getTransform().getScale()[
axisIndex
];
scaleState.startDistFromOrigin =
vec3.dist(worldCoords, model.activeState.getOrigin()) || 0.0001;
}
};

publicAPI.handleRotateStartEvent = (callData, axis) => {
model.rotationManipulator.setHandleOrigin(model.activeState.getOrigin());
model.rotationManipulator.setHandleNormal(model.activeState.getDirection());

// compute unit vector from center of rotation
// to the click point on the plane defined by
// the center of rotation and the rotation normal.
const { worldCoords } = model.rotationManipulator.handleEvent(
callData,
model._apiSpecificRenderWindow
);

if (worldCoords.length) {
isDragging = true;
vec3.sub(
rotateState.dragStartVec,
worldCoords,
model.activeState.getOrigin()
);
vec3.normalize(rotateState.dragStartVec, rotateState.dragStartVec);

rotateState.startQuat = model.widgetState.getTransform().getRotation();
}
};

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

publicAPI.handleLeftButtonRelease = () => {
if (isDragging && model.pickable) {
model._interactor.cancelAnimation(publicAPI);
}
isDragging = false;
model.widgetState.deactivate();
};

publicAPI.handleEvent = (callData) => {
if (model.pickable && model.activeState && model.activeState.getActive()) {
const [type, axis] = model.activeState.getName().split(':');
const axisIndex = 'XYZ'.indexOf(axis);
if (type === 'translate') {
return publicAPI.handleTranslateEvent(callData, axis, axisIndex);
}
if (type === 'scale') {
return publicAPI.handleScaleEvent(callData, axis, axisIndex);
}
if (type === 'rotate') {
return publicAPI.handleRotateEvent(callData, axis, axisIndex);
}
}
return macro.VOID;
};

publicAPI.handleTranslateEvent = (callData, axis, axisIndex) => {
model.lineManipulator.setHandleOrigin(model.activeState.getOrigin());
model.lineManipulator.setHandleNormal(model.activeState.getDirection());

const { worldCoords } = model.lineManipulator.handleEvent(
callData,
model._apiSpecificRenderWindow
);

if (worldCoords.length) {
const positiveDir = [0, 0, 0];
positiveDir[axisIndex] = 1;

const toWorldCoords = [0, 0, 0];
vec3.sub(toWorldCoords, worldCoords, translateState.dragStartCoord);

const dir = Math.sign(vec3.dot(positiveDir, toWorldCoords));
const dist = vec3.len(toWorldCoords);
const delta = dir * dist;

const translation = model.widgetState.getTransform().getTranslation();
translation[axisIndex] = translateState.startPos + delta;
model.widgetState.getTransform().setTranslation(translation);
}
};

publicAPI.handleScaleEvent = (callData, axis, axisIndex) => {
model.lineManipulator.setHandleOrigin(model.activeState.getOrigin());
model.lineManipulator.setHandleNormal(model.activeState.getDirection());

const { worldCoords } = model.lineManipulator.handleEvent(
callData,
model._apiSpecificRenderWindow
);

if (worldCoords.length) {
const dist = vec3.dist(model.activeState.getOrigin(), worldCoords);
const scale =
(dist / scaleState.startDistFromOrigin) * scaleState.startScale;

const scales = model.widgetState.getTransform().getScale();
scales[axisIndex] = scale;
model.widgetState.getTransform().setScale(scales);
}
};

publicAPI.handleRotateEvent = (callData) => {
model.rotationManipulator.setHandleOrigin(model.activeState.getOrigin());
model.rotationManipulator.setHandleNormal(model.activeState.getDirection());

const { worldCoords } = model.rotationManipulator.handleEvent(
callData,
model._apiSpecificRenderWindow
);

const curPointerVec = [0, 0, 0];
if (worldCoords.length) {
vec3.sub(curPointerVec, worldCoords, model.activeState.getOrigin());
vec3.normalize(curPointerVec, curPointerVec);

const angle = vec3.angle(rotateState.dragStartVec, curPointerVec);

const signVec = [0, 0, 0];
vec3.cross(signVec, curPointerVec, rotateState.dragStartVec);
vec3.normalize(signVec, signVec);
const sign = vec3.dot(signVec, model.activeState.getDirection());

const q = quat.create();
quat.setAxisAngle(q, model.activeState.getDirection(), -sign * angle);

quat.mul(q, q, rotateState.startQuat);
quat.normalize(q, q);

// do not amplify fp errors when editing a particular direction
const direction = model.activeState.getDirection();
model.widgetState.getTransform().setRotation(q);
model.activeState.setDirection(direction);
}

return macro.EVENT_ABORT;
};

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

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

model.classHierarchy.push('vtkTransformControlsWidgetProp');
}
constants.js
export const ROTATE_HANDLE_PIXEL_SCALE = 240;
export const TRANSLATE_HANDLE_RADIUS = 3;
export const SCALE_HANDLE_RADIUS = 3;
export const SCALE_HANDLE_CUBE_SIDE_LENGTH = 20;
export const SCALE_HANDLE_PIXEL_SCALE = 320;

export const TransformMode = {
TRANSLATE: 'translate',
SCALE: 'scale',
ROTATE: 'rotate',
};

export default {
TransformMode,
};
index.js
import { mat3 } from 'gl-matrix';
import macro from 'vtk.js/Sources/macros';
import vtkAbstractWidgetFactory from 'vtk.js/Sources/Widgets/Core/AbstractWidgetFactory';

import vtkTranslateTransformHandleRepresentation from 'vtk.js/Sources/Widgets/Representations/TranslateTransformHandleRepresentation';
import vtkScaleTransformHandleRepresentation from 'vtk.js/Sources/Widgets/Representations/ScaleTransformHandleRepresentation';
import vtkRotateTransformHandleRepresentation from 'vtk.js/Sources/Widgets/Representations/RotateTransformHandleRepresentation';

import {
TRANSLATE_HANDLE_RADIUS,
SCALE_HANDLE_RADIUS,
SCALE_HANDLE_CUBE_SIDE_LENGTH,
SCALE_HANDLE_PIXEL_SCALE,
TransformMode,
} from './constants';

import widgetBehavior from './behavior';
import stateGenerator from './state';

function updateHandleTransforms(widgetState) {
const transformState = widgetState.getTransform();

const sx = widgetState.getScaleHandleX();
const sy = widgetState.getScaleHandleY();
const sz = widgetState.getScaleHandleZ();

const hx = widgetState.getRotateHandleX();
const hy = widgetState.getRotateHandleY();
const hz = widgetState.getRotateHandleZ();

// translation
widgetState.getStatesWithLabel('handles').forEach((state) => {
state.setOrigin(transformState.getTranslation());
});

// rotation
const m3 = mat3.create();
mat3.fromQuat(m3, transformState.getRotation());

[sx, hx].forEach((state) => {
state.setDirection(m3.slice(0, 3));
state.setUp(m3.slice(3, 6).map((c) => -c));
state.setRight(m3.slice(6, 9));
});

[sy, hy].forEach((state) => {
state.setDirection(m3.slice(3, 6));
state.setUp(m3.slice(6, 9));
state.setRight(m3.slice(0, 3));
});

[sz, hz].forEach((state) => {
state.setDirection(m3.slice(6, 9));
state.setUp(m3.slice(3, 6));
state.setRight(m3.slice(0, 3));
});
}

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

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

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

model.behavior = widgetBehavior;
model.widgetState = stateGenerator();

model.methodsToLink = [
'scaleInPixels',
'activeScaleFactor',
'useActiveColor',
'activeColor',
];

publicAPI.getRepresentationsForViewType = (viewType) => {
switch (viewType) {
default:
return [
{
builder: vtkTranslateTransformHandleRepresentation,
labels: ['translateHandles'],
initialValues: {
radius: TRANSLATE_HANDLE_RADIUS,
glyphResolution: 12,
coneSource: {
radius: 8,
height: 0.05,
direction: [0, 1, 0],
},
},
},
{
builder: vtkScaleTransformHandleRepresentation,
labels: ['scaleHandles'],
initialValues: {
radius: SCALE_HANDLE_RADIUS,
glyphResolution: 12,
cubeSource: {
xLength: SCALE_HANDLE_CUBE_SIDE_LENGTH,
yLength:
SCALE_HANDLE_CUBE_SIDE_LENGTH / SCALE_HANDLE_PIXEL_SCALE,
zLength: SCALE_HANDLE_CUBE_SIDE_LENGTH,
},
},
},
{
builder: vtkRotateTransformHandleRepresentation,
labels: ['rotateHandles'],
},
];
}
};

publicAPI.updateHandleVisibility = () => {
model.widgetState
.getStatesWithLabel('translateHandles')
.forEach((state) => {
state.setVisible(model.mode === 'translate');
});
model.widgetState.getStatesWithLabel('scaleHandles').forEach((state) => {
state.setVisible(model.mode === 'scale');
});
model.widgetState.getStatesWithLabel('rotateHandles').forEach((state) => {
state.setVisible(model.mode === 'rotate');
});
};

model._onModeChanged = () => {
publicAPI.updateHandleVisibility();
};

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

// sync translation/scale/rotation states to the handle states
const transformSubscription = model.widgetState
.getTransform()
.onModified((state) => {
updateHandleTransforms(model.widgetState);
});

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

updateHandleTransforms(model.widgetState);
publicAPI.updateHandleVisibility();
}

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

const DEFAULT_VALUES = {
mode: TransformMode.TRANSLATE,
};

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

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

vtkAbstractWidgetFactory.extend(publicAPI, model, initialValues);

macro.setGet(publicAPI, model, ['mode']);
macro.get(publicAPI, model, ['lineManipulator', 'rotateManipulator']);
vtkTransformControlsWidget(publicAPI, model);
}

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

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

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

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

export default function stateGenerator() {
const transformState = vtkStateBuilder
.createBuilder()
.addField({
name: 'translation',
initialValue: [0, 0, 0],
})
.addField({
name: 'scale',
initialValue: [1, 1, 1],
})
.addField({
name: 'rotation',
initialValue: [0, 0, 0, 1],
})
.build();

return (
vtkStateBuilder
.createBuilder()
.addStateFromInstance({
labels: [],
name: 'transform',
instance: transformState,
})

// translate state
.addStateFromMixin({
labels: ['handles', 'translateHandles'],
mixins: [
'name',
'origin',
'color3',
'scale3',
'orientation',
'visible',
],
name: 'translateHandleZ',
initialValues: {
name: 'translate:Z',
scale3: [1, 1, SCALE_HANDLE_PIXEL_SCALE],
origin: [0, 0, 0],
color3: [0, 255, 0],
// these are fixed to the world axes
up: [0, 1, 0],
right: [1, 0, 0],
direction: [0, 0, 1],
},
})
.addStateFromMixin({
labels: ['handles', 'translateHandles'],
mixins: [
'name',
'origin',
'color3',
'scale3',
'orientation',
'visible',
],
name: 'translateHandleX',
initialValues: {
name: 'translate:X',
scale3: [1, 1, SCALE_HANDLE_PIXEL_SCALE],
origin: [0, 0, 0],
color3: [0, 0, 255],
// these are fixed to the world axes
up: [0, 1, 0],
right: [0, 0, -1],
direction: [1, 0, 0],
},
})
.addStateFromMixin({
labels: ['handles', 'translateHandles'],
mixins: [
'name',
'origin',
'color3',
'scale3',
'orientation',
'visible',
],
name: 'translateHandleY',
initialValues: {
name: 'translate:Y',
scale3: [1, 1, SCALE_HANDLE_PIXEL_SCALE],
origin: [0, 0, 0],
color3: [255, 0, 0],
// these are fixed to the world axes
up: [0, 0, 1],
right: [1, 0, 0],
direction: [0, 1, 0],
},
})

// scale state
.addStateFromMixin({
labels: ['handles', 'scaleHandles'],
mixins: [
'name',
'origin',
'color3',
'scale3',
'orientation',
'visible',
],
name: 'scaleHandleZ',
initialValues: {
name: 'scale:Z',
scale3: [1, 1, SCALE_HANDLE_PIXEL_SCALE],
origin: [0, 0, 0],
color3: [0, 255, 0],
// these are set via setHandleOrientationsFromQuat
up: [0, 0, 1],
right: [0, 1, 0],
direction: [1, 0, 0],
},
})
.addStateFromMixin({
labels: ['handles', 'scaleHandles'],
mixins: [
'name',
'origin',
'color3',
'scale3',
'orientation',
'visible',
],
name: 'scaleHandleX',
initialValues: {
name: 'scale:X',
scale3: [1, 1, SCALE_HANDLE_PIXEL_SCALE],
origin: [0, 0, 0],
color3: [0, 0, 255],
// these are set via setHandleOrientationsFromQuat
up: [1, 0, 0],
right: [0, -1, 0],
direction: [0, 0, 1],
},
})
.addStateFromMixin({
labels: ['handles', 'scaleHandles'],
mixins: [
'name',
'origin',
'color3',
'scale3',
'orientation',
'visible',
],
name: 'scaleHandleY',
initialValues: {
name: 'scale:Y',
scale3: [1, 1, SCALE_HANDLE_PIXEL_SCALE],
origin: [0, 0, 0],
color3: [255, 0, 0],
// these are set via setHandleOrientationsFromQuat
up: [0, 1, 0],
right: [1, 0, 0],
direction: [0, 0, 1],
},
})

// rotation state
.addStateFromMixin({
labels: ['handles', 'rotateHandles'],
mixins: [
'name',
'origin',
'color3',
'scale1',
'orientation',
'visible',
],
name: 'rotateHandleZ',
initialValues: {
name: 'rotate:Z',
scale1: ROTATE_HANDLE_PIXEL_SCALE,
origin: [0, 0, 0],
color3: [0, 255, 0],
// these are set via setHandleOrientationsFromQuat
up: [0, 1, 0],
right: [1, 0, 0],
direction: [0, 0, 1],
},
})
.addStateFromMixin({
labels: ['handles', 'rotateHandles'],
mixins: [
'name',
'origin',
'color3',
'scale1',
'orientation',
'visible',
],
name: 'rotateHandleX',
initialValues: {
name: 'rotate:X',
scale1: ROTATE_HANDLE_PIXEL_SCALE,
origin: [0, 0, 0],
color3: [0, 0, 255],
// these are set via setHandleOrientationsFromQuat
up: [0, 1, 0],
right: [0, 0, -1],
direction: [1, 0, 0],
},
})
.addStateFromMixin({
labels: ['handles', 'rotateHandles'],
mixins: [
'name',
'origin',
'color3',
'scale1',
'orientation',
'visible',
],
name: 'rotateHandleY',
initialValues: {
name: 'rotate:Y',
scale1: ROTATE_HANDLE_PIXEL_SCALE,
origin: [0, 0, 0],
color3: [255, 0, 0],
// these are set via setHandleOrientationsFromQuat
up: [0, 0, 1],
right: [1, 0, 0],
direction: [0, 1, 0],
},
})
.build()
);
}