LineWidget

Source

Constants.js
export const Direction = [0, 0, 0];

export const HandleRepresentation = [0, 0, 0];

export const HandleBehavior = {
HANDLE1_ALONE: 3,
HANDLE2: 2,
HANDLE1: 1,
};

export const HandleRepresentationType = {
// 3D handles
SPHERE: 'sphere',
CUBE: 'cube',
CONE: 'cone',
NONE: 'sphere',
// 2D handles
ARROWHEAD3: 'triangle',
ARROWHEAD4: '4pointsArrowHead',
ARROWHEAD6: '6pointsArrowHead',
STAR: 'star',
CIRCLE: 'circle',
};

export default {
HandleBehavior,
Direction,
HandleRepresentation,
HandleRepresentationType,
};
behavior.js
import Constants from 'vtk.js/Sources/Widgets/Widgets3D/LineWidget/Constants';
import macro from 'vtk.js/Sources/macro';
import * as vtkMath from 'vtk.js/Sources/Common/Core/Math/';

import {
calculateTextPosition,
updateTextPosition,
} from 'vtk.js/Sources/Widgets/Widgets3D/LineWidget/helper';

const MAX_POINTS = 2;

const { Direction, HandleBehavior, HandleRepresentationType } = Constants;

export default function widgetBehavior(publicAPI, model) {
model.classHierarchy.push('vtkLineWidgetProp');
let isDragging = null;

// --------------------------------------------------------------------------
// Display 2D
// --------------------------------------------------------------------------

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

// --------------------------------------------------------------------------
// Interactor event
// --------------------------------------------------------------------------

function ignoreKey(e) {
return e.altKey || e.controlKey || e.shiftKey;
}

/*
* check for handle 2 position in comparison to handle 1 position
* and sets text offset to not overlap on the line representation
*/
function calcTextPosWithLineAngle() {
const pos1 = model.widgetState.getHandle1().getOrigin();
const pos2 = model.widgetState.getHandle2().getOrigin();
const SVGTextProps = model.representations[2].getTextProps();

let dySign = 1;
if (pos1[0] <= pos2[0]) {
dySign = pos1[1] <= pos2[1] ? 1 : -1;
} else {
dySign = pos1[1] <= pos2[1] ? -1 : 1;
}
SVGTextProps.dy = dySign * Math.abs(SVGTextProps.dy);
}

function updateHandleDirection(behavior, callData) {
let bv = behavior;
if (bv === HandleBehavior.HANDLE1_ALONE) {
const handle1Pos = model.widgetState.getHandle1().getOrigin();
const WorldMousePos = publicAPI.computeWorldToDisplay(
model.renderer,
handle1Pos[0],
handle1Pos[1],
handle1Pos[2]
);
const mousePos = publicAPI.computeDisplayToWorld(
model.renderer,
callData.position.x,
callData.position.y,
WorldMousePos[2]
);
vtkMath.subtract(
model.widgetState.getHandle1().getOrigin(),
mousePos,
Direction
);
bv = 0;
} else {
const modifier = bv === 1 ? 1 : -1;
bv -= 1;
const handle1Pos = model.widgetState.getHandle1().getOrigin();
const handle2Pos = model.widgetState.getHandle2().getOrigin();
vtkMath.subtract(handle1Pos, handle2Pos, Direction);
vtkMath.multiplyScalar(Direction, modifier);
}
model.representations[bv].getGlyph().setDirection(Direction);
}

function isHandleOrientable(handleType) {
if (
handleType === HandleRepresentationType.CONE ||
handleType === HandleRepresentationType.ARROWHEAD3 ||
handleType === HandleRepresentationType.ARROWHEAD4 ||
handleType === HandleRepresentationType.ARROWHEAD6
)
return 1;
return 0;
}

function isOrientable() {
return (
isHandleOrientable(model.handle1Shape) ||
isHandleOrientable(model.handle2Shape)
);
}

// set in public to update handle Direction when handle change in UI
publicAPI.setHandleDirection = () => {
if (isHandleOrientable(model.handle1Shape)) {
updateHandleDirection(HandleBehavior.HANDLE1);
}
if (isHandleOrientable(model.handle2Shape)) {
updateHandleDirection(HandleBehavior.HANDLE2);
}
};

// --------------------------------------------------------------------------
// Left press: Select handle to drag
// --------------------------------------------------------------------------

publicAPI.handleLeftButtonPress = (e) => {
if (
!model.activeState ||
!model.activeState.getActive() ||
!model.pickable ||
ignoreKey(e)
) {
return macro.VOID;
}
const moveHandle = model.widgetState.getMoveHandle();
moveHandle.setVisible(false);
if (
model.activeState === model.widgetState.getMoveHandle() &&
model.widgetState.getNbHandles() === 0
) {
const handle1 = model.widgetState.getHandle1();
model.widgetState.setNbHandles(1);
handle1.setOrigin(...moveHandle.getOrigin());
handle1.setColor(moveHandle.getColor());
handle1.setScale1(moveHandle.getScale1());
handle1.setVisible(true);
model.widgetState.getHandle2().setOrigin(...moveHandle.getOrigin());
const SVGLayerText = model.widgetState.getText();
SVGLayerText.setText(model.text);
SVGLayerText.setOrigin(
calculateTextPosition(model, model.widgetState.getPositionOnLine())
);
} else if (
model.activeState === model.widgetState.getMoveHandle() &&
model.widgetState.getNbHandles() === 1
) {
model.widgetState.setNbHandles(2);
const handle2 = model.widgetState.getHandle2();
handle2.setOrigin(...moveHandle.getOrigin());
handle2.setColor(moveHandle.getColor());
handle2.setScale1(moveHandle.getScale1());
handle2.setVisible(true);
publicAPI.setHandleDirection();
const SVGLayerText = model.widgetState.getText();
SVGLayerText.setText(model.text);
SVGLayerText.setOrigin(
calculateTextPosition(model, model.widgetState.getPositionOnLine())
);
calcTextPosWithLineAngle();
moveHandle.setVisible(true);
} else {
isDragging = true;
model.openGLRenderWindow.setCursor('grabbing');
model.interactor.requestAnimation(publicAPI);
}
publicAPI.invokeStartInteractionEvent();
return macro.EVENT_ABORT;
};

// --------------------------------------------------------------------------
// Mouse move: Drag selected handle / Handle follow the mouse
// --------------------------------------------------------------------------

publicAPI.handleMouseMove = (callData) => {
if (model.hasFocus && model.widgetState.getNbHandles() === MAX_POINTS) {
publicAPI.loseFocus();
return macro.VOID;
}
if (
model.pickable &&
model.manipulator &&
model.activeState &&
model.activeState.getActive() &&
!ignoreKey(callData)
) {
const worldCoords = model.manipulator.handleEvent(
callData,
model.openGLRenderWindow
);
if (model.widgetState.getNbHandles() === 1) {
model.widgetState.getMoveHandle().setVisible(true);
}
if (
model.activeState === model.widgetState.getMoveHandle() ||
isDragging
) {
model.activeState.setOrigin(worldCoords);
publicAPI.invokeInteractionEvent();
if (isDragging === true) {
if (isOrientable()) {
updateTextPosition(model, model.widgetState.getPositionOnLine());
calcTextPosWithLineAngle();
publicAPI.setHandleDirection();
}
} else if (
model.widgetState.getNbHandles() === 1 &&
isHandleOrientable(model.handle1Shape)
) {
updateHandleDirection(HandleBehavior.HANDLE1_ALONE, callData);
}
return macro.EVENT_ABORT;
}
}
return macro.VOID;
};

// --------------------------------------------------------------------------
// Left release: Finish drag / Create new handle
// --------------------------------------------------------------------------

publicAPI.handleLeftButtonRelease = () => {
if (isDragging && model.pickable) {
calcTextPosWithLineAngle();
model.openGLRenderWindow.setCursor('pointer');
model.widgetState.deactivate();
model.interactor.cancelAnimation(publicAPI);
publicAPI.invokeEndInteractionEvent();
} else if (model.activeState !== model.widgetState.getMoveHandle()) {
model.widgetState.deactivate();
}
if (
(model.hasFocus && !model.activeState) ||
(model.activeState && !model.activeState.getActive())
) {
publicAPI.invokeEndInteractionEvent();
model.widgetManager.enablePicking();
model.interactor.render();
}
isDragging = false;
};

// --------------------------------------------------------------------------
// Focus API - modeHandle follow mouse when widget has focus
// --------------------------------------------------------------------------

publicAPI.grabFocus = () => {
if (!model.hasFocus && model.widgetState.getNbHandles() < MAX_POINTS) {
model.activeState = model.widgetState.getMoveHandle();
model.activeState.activate();
model.interactor.requestAnimation(publicAPI);
publicAPI.invokeStartInteractionEvent();
}
model.hasFocus = true;
};

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

publicAPI.loseFocus = () => {
if (model.hasFocus) {
model.interactor.cancelAnimation(publicAPI);
publicAPI.invokeEndInteractionEvent();
}
model.widgetState.deactivate();
model.widgetState.getMoveHandle().deactivate();
model.widgetState.getMoveHandle().setVisible(false);
model.activeState = null;
model.hasFocus = false;
model.widgetManager.enablePicking();
model.interactor.render();
};
}
helper.js
import * as vtkMath from 'vtk.js/Sources/Common/Core/Math/';

export function calculateTextPosition(model) {
const vector = [0, 0, 0];
const handle1WorldPos = model.widgetState.getHandle1().getOrigin();
const handle2WorldPos = model.widgetState.getHandle2().getOrigin();
let statePositionOnLine = model.widgetState.getPositionOnLine();
statePositionOnLine = 1 - statePositionOnLine;
vtkMath.subtract(handle1WorldPos, handle2WorldPos, vector);
vtkMath.multiplyScalar(vector, statePositionOnLine);
vtkMath.add(vector, handle2WorldPos, vector);
return vector;
}

export function updateTextPosition(model) {
const SVGTextState = model.widgetState.getText();
SVGTextState.setOrigin(calculateTextPosition(model));
}
index.js
import Constants from 'vtk.js/Sources/Widgets/Widgets3D/LineWidget/Constants';
import { distance2BetweenPoints } from 'vtk.js/Sources/Common/Core/Math';
import macro from 'vtk.js/Sources/macro';
import stateGenerator from 'vtk.js/Sources/Widgets/Widgets3D/LineWidget/state';
import vtkAbstractWidgetFactory from 'vtk.js/Sources/Widgets/Core/AbstractWidgetFactory';
import vtkArrowHandleRepresentation from 'vtk.js/Sources/Widgets/Representations/ArrowHandleRepresentation';
import vtkPlanePointManipulator from 'vtk.js/Sources/Widgets/Manipulators/PlaneManipulator';
import vtkSphereHandleRepresentation from 'vtk.js/Sources/Widgets/Representations/SphereHandleRepresentation';
import vtkCircleHandleRepresentation from 'vtk.js/Sources/Widgets/Representations/CircleHandleRepresentation';
import vtkCubeHandleRepresentation from 'vtk.js/Sources/Widgets/Representations/CubeHandleRepresentation';
import vtkConeHandleRepresentation from 'vtk.js/Sources/Widgets/Representations/ConeHandleRepresentation';
import vtkSVGLandmarkRepresentation from 'vtk.js/Sources/Widgets/SVG/SVGLandmarkRepresentation';
import vtkPolyLineRepresentation from 'vtk.js/Sources/Widgets/Representations/PolyLineRepresentation';
import widgetBehavior from 'vtk.js/Sources/Widgets/Widgets3D/LineWidget/behavior';
import { ViewTypes } from 'vtk.js/Sources/Widgets/Core/WidgetManager/Constants';

import { updateTextPosition } from 'vtk.js/Sources/Widgets/Widgets3D/LineWidget/helper';

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

const { HandleRepresentationType, HandleRepresentation } = Constants;

const shapeToRepresentation = {};

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

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

// custom handles set in default values
// 3D source handles
shapeToRepresentation[
HandleRepresentationType.SPHERE
] = vtkSphereHandleRepresentation;
shapeToRepresentation[
HandleRepresentationType.CUBE
] = vtkCubeHandleRepresentation;
shapeToRepresentation[
HandleRepresentationType.CONE
] = vtkConeHandleRepresentation;
shapeToRepresentation[
HandleRepresentationType.NONE
] = vtkSphereHandleRepresentation;
// 2D source handles
shapeToRepresentation[
HandleRepresentationType.ARROWHEAD3
] = vtkArrowHandleRepresentation;
shapeToRepresentation[
HandleRepresentationType.ARROWHEAD4
] = vtkArrowHandleRepresentation;
shapeToRepresentation[
HandleRepresentationType.ARROWHEAD6
] = vtkArrowHandleRepresentation;
shapeToRepresentation[
HandleRepresentationType.STAR
] = vtkArrowHandleRepresentation;
shapeToRepresentation[
HandleRepresentationType.CIRCLE
] = vtkCircleHandleRepresentation;

function initializeHandleRepresentations() {
HandleRepresentation[0] = shapeToRepresentation[model.handle1Shape];
if (!HandleRepresentation[0]) {
HandleRepresentation[0] = vtkSphereHandleRepresentation;
}
HandleRepresentation[1] = shapeToRepresentation[model.handle2Shape];
if (!HandleRepresentation[1]) {
HandleRepresentation[1] = vtkSphereHandleRepresentation;
}
}

model.methodsToLink = [
'activeScaleFactor',
'activeColor',
'useActiveColor',
'glyphResolution',
'defaultScale',
];
model.behavior = widgetBehavior;
model.widgetState = stateGenerator();
model.widgetState.setPositionOnLine(model.positionOnLine);
model.widgetState.setNbHandles(0);
initializeHandleRepresentations();

publicAPI.getRepresentationsForViewType = (viewType) => {
switch (viewType) {
case ViewTypes.DEFAULT:
case ViewTypes.GEOMETRY:
case ViewTypes.SLICE:
case ViewTypes.VOLUME:
default:
return [
{
builder: HandleRepresentation[0],
labels: ['handle1'],
initialValues: {
/* to scale handle size when zooming/dezooming, optionnal */
scaleInPixels: true,
/* to detect arrow type in ArrowHandleRepresentation, mandatory */
handleType: model.handle1Shape,
},
},
{
builder: HandleRepresentation[1],
labels: ['handle2'],
initialValues: {
/* to scale handle size when zooming/dezooming, optionnal */
scaleInPixels: true,
/* to detect arrow type in ArrowHandleRepresentation, mandatory */
handleType: model.handle2Shape,
},
},
{
builder: vtkSVGLandmarkRepresentation,
initialValues: {
showCircle: false,
isVisible: false,
},
labels: ['SVGtext'],
},
{
builder: vtkPolyLineRepresentation,
labels: ['handle1', 'handle2', 'moveHandle'],
initialValues: { scaleInPixels: true },
},
];
}
};

// --- Public methods -------------------------------------------------------

publicAPI.getDistance = () => {
const nbHandles =
model.widgetState.getHandle1List().length +
model.widgetState.getHandle2List().length;
if (nbHandles !== 2) {
return 0;
}
return Math.sqrt(
distance2BetweenPoints(
model.widgetState.getHandle1List()[0].getOrigin(),
model.widgetState.getHandle2List()[0].getOrigin()
)
);
};

publicAPI.updateTextValue = (text) => {
if (typeof model.widgetState.getText() !== 'undefined')
model.widgetState.getText().setText(text);
};

publicAPI.updateTextProps = (input, prop) => {
if (prop === 'positionOnLine') {
publicAPI.setPositionOnLine(input / 100);
}
updateTextPosition(model, publicAPI.getPositionOnLine());
model.widgetState.setPositionOnLine(publicAPI.getPositionOnLine());
};

publicAPI.updateHandleFromUI = (input, handleId) => {
if (handleId === 1) {
model.handle1Shape = input;
} else if (handleId === 2) {
model.handle2Shape = input;
}
initializeHandleRepresentations();
publicAPI.getRepresentationsForViewType(0);
};

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

model.widgetState.onBoundsChange((bounds) => {
const center = [
(bounds[0] + bounds[1]) * 0.5,
(bounds[2] + bounds[3]) * 0.5,
(bounds[4] + bounds[5]) * 0.5,
];
model.widgetState.getMoveHandle().setOrigin(center);
});

// Default manipulator
model.manipulator = vtkPlanePointManipulator.newInstance();
}

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

const DEFAULT_VALUES = {
handle1Shape: HandleRepresentationType.ARROWHEAD6,
handle2Shape: HandleRepresentationType.ARROWHEAD6,
/* Position of the text on the line where 0 is handle1 and 1 is handle2 */
positionOnLine: 0.5,
/* You can change the initial value of the text here, the initialValue variable
* of the state is meant to create an empty text to insert the desired text
* when both handles are set, and avoids having a default text before
*/
text: 'Text orginal',
};

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

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

vtkAbstractWidgetFactory.extend(publicAPI, model, initialValues);
macro.setGet(publicAPI, model, [
'manipulator',
'handle1Shape',
'handle2Shape',
'positionOnLine',
]);
vtkLineWidget(publicAPI, model);
}

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

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

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

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

export default function generateState() {
return vtkStateBuilder
.createBuilder()
.addStateFromMixin({
labels: ['moveHandle'],
mixins: ['origin', 'color', 'scale1', 'visible'],
name: 'moveHandle',
initialValues: {
scale1: 50,
visible: false,
origin: [],
},
})
.addStateFromMixin({
labels: ['handle1'],
mixins: ['origin', 'color', 'scale1', 'visible'],
name: 'handle1',
initialValues: {
scale1: 50,
origin: [],
visible: false,
},
})
.addStateFromMixin({
labels: ['handle2'],
mixins: ['origin', 'color', 'scale1', 'visible'],
name: 'handle2',
initialValues: {
scale1: 50,
origin: [],
visible: false,
},
})
.addStateFromMixin({
labels: ['SVGtext'],
mixins: ['origin', 'color', 'text', 'visible'],
name: 'text',
initialValues: {
/* text is empty to set a text filed in the SVGLayer and to avoid
* displaying text before positionning the handles */
text: '',
visible: false,
origin: [0, 0, 0],
},
})
.addField({ name: 'positionOnLine', initialValues: 0 })
.addField({ name: 'nbHandles', initialValues: 0 })
.build();
}