SplineWidget

Source

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

export default function widgetBehavior(publicAPI, model) {
model.classHierarchy.push('vtkSplineWidgetProp');
model._isDragging = false;

model.keysDown = {};
model.moveHandle = model.widgetState.getMoveHandle();

// --------------------------------------------------------------------------
// Private methods
// --------------------------------------------------------------------------

const updateHandlesSize = () => {
if (publicAPI.getHandleSizeInPixels() != null) {
const scale = publicAPI.getHandleSizeInPixels();

model.moveHandle.setScale1(scale);
model.widgetState.getHandleList().forEach((handle) => {
handle.setScale1(scale);
});
}
};

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

const addPoint = () => {
// Commit handle to location
if (
!model.lastHandle ||
model.keysDown.Control ||
!model.freeHand ||
vec3.squaredDistance(
model.moveHandle.getOrigin(),
model.lastHandle.getOrigin()
) >
publicAPI.getFreehandMinDistance() * publicAPI.getFreehandMinDistance()
) {
model.lastHandle = model.widgetState.addHandle();
model.lastHandle.setVisible(false);
model.lastHandle.setOrigin(...model.moveHandle.getOrigin());
model.lastHandle.setColor(model.moveHandle.getColor());
model.lastHandle.setScale1(model.moveHandle.getScale1());
model.lastHandle.setManipulator(model.manipulator);

if (!model.firstHandle) {
model.firstHandle = model.lastHandle;
}

model._apiSpecificRenderWindow.setCursor('grabbing');
}
};

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

const getHoveredHandle = () => {
const handles = model.widgetState.getHandleList();

const scale =
model.moveHandle.getScale1() *
vec3.distance(
model._apiSpecificRenderWindow.displayToWorld(0, 0, 0, model._renderer),
model._apiSpecificRenderWindow.displayToWorld(1, 0, 0, model._renderer)
);

return handles.reduce(
({ closestHandle, closestDistance }, handle) => {
if (
handle !== model.moveHandle &&
model.moveHandle.getOrigin() &&
handle.getOrigin()
) {
const distance = vec3.squaredDistance(
model.moveHandle.getOrigin(),
handle.getOrigin()
);
if (distance < closestDistance) {
return {
closestHandle: handle,
closestDistance: distance,
};
}
}

return {
closestHandle,
closestDistance,
};
},
{
closestHandle: null,
closestDistance: scale * scale,
}
).closestHandle;
};

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

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

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

publicAPI.setResetAfterPointPlacement =
model._factory.setResetAfterPointPlacement;
publicAPI.getResetAfterPointPlacement =
model._factory.getResetAfterPointPlacement;
publicAPI.setResetAfterPointPlacement(
publicAPI.getResetAfterPointPlacement()
);

publicAPI.setFreehandMinDistance = model._factory.setFreehandMinDistance;
publicAPI.getFreehandMinDistance = model._factory.getFreehandMinDistance;
publicAPI.setFreehandMinDistance(publicAPI.getFreehandMinDistance());

publicAPI.setAllowFreehand = model._factory.setAllowFreehand;
publicAPI.getAllowFreehand = model._factory.getAllowFreehand;
publicAPI.setAllowFreehand(publicAPI.getAllowFreehand());

publicAPI.setDefaultCursor = model._factory.setDefaultCursor;
publicAPI.getDefaultCursor = model._factory.getDefaultCursor;
publicAPI.setDefaultCursor(publicAPI.getDefaultCursor());

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

publicAPI.setHandleSizeInPixels = (size) => {
model._factory.setHandleSizeInPixels(size);
updateHandlesSize();
};
publicAPI.getHandleSizeInPixels = model._factory.getHandleSizeInPixels;
publicAPI.setHandleSizeInPixels(model._factory.getHandleSizeInPixels()); // set initial value

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

publicAPI.setResolution = (resolution) => {
model._factory.setResolution(resolution);
model.representations[1].setResolution(resolution);
};
publicAPI.setResolution(model._factory.getResolution()); // set initial value

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

publicAPI.getPoints = () =>
model.representations[1].getOutputData().getPoints().getData();

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

publicAPI.reset = () => {
model.widgetState.clearHandleList();

model.lastHandle = null;
model.firstHandle = null;
};

// --------------------------------------------------------------------------
// Right click: Delete handle
// --------------------------------------------------------------------------

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

if (model.activeState !== model.moveHandle) {
model._interactor.requestAnimation(publicAPI);
model.activeState.deactivate();
model.widgetState.removeHandle(model.activeState);
model.activeState = null;
model._interactor.cancelAnimation(publicAPI);
} else {
const handle = getHoveredHandle();
if (handle) {
model.widgetState.removeHandle(handle);
} else if (model.lastHandle) {
model.widgetState.removeHandle(model.lastHandle);
const handles = model.widgetState.getHandleList();
model.lastHandle = handles[handles.length - 1];
}
}

publicAPI.invokeInteractionEvent();

return macro.EVENT_ABORT;
};

// --------------------------------------------------------------------------
// Left press: Add new point
// --------------------------------------------------------------------------

publicAPI.handleLeftButtonPress = (e) => {
const manipulator =
model.activeState?.getManipulator?.() ?? model.manipulator;
if (
!manipulator ||
!model.activeState ||
!model.activeState.getActive() ||
!model.pickable
) {
return macro.VOID;
}

// update worldDelta
manipulator.handleEvent(e, model._apiSpecificRenderWindow);

if (model.activeState === model.moveHandle) {
if (model.widgetState.getHandleList().length === 0) {
addPoint();
} else {
const hoveredHandle = getHoveredHandle();
if (hoveredHandle && !model.keysDown.Control) {
model.moveHandle.deactivate();
model.moveHandle.setVisible(false);
model.activeState = hoveredHandle;
hoveredHandle.activate();

model._isDragging = true;
model.lastHandle.setVisible(true);
} else {
addPoint();
}
}

model.freeHand = publicAPI.getAllowFreehand() && !model._isDragging;
} else if (model.dragable) {
model._isDragging = true;
model._apiSpecificRenderWindow.setCursor('grabbing');
model._interactor.requestAnimation(publicAPI);
}

publicAPI.invokeStartInteractionEvent();
return macro.EVENT_ABORT;
};

// --------------------------------------------------------------------------
// Left release
// --------------------------------------------------------------------------

publicAPI.handleLeftButtonRelease = (e) => {
if (model._isDragging) {
if (!model.hasFocus) {
model._apiSpecificRenderWindow.setCursor(model.defaultCursor);
model.widgetState.deactivate();
model._interactor.cancelAnimation(publicAPI);
publicAPI.invokeEndInteractionEvent();
} else {
model.moveHandle.setOrigin(...model.activeState.getOrigin());
model.activeState.deactivate();
model.moveHandle.activate();
model.activeState = model.moveHandle;

if (!model.draggedPoint) {
if (
vec3.squaredDistance(
model.moveHandle.getOrigin(),
model.lastHandle.getOrigin()
) <
model.moveHandle.getScale1() * model.moveHandle.getScale1() ||
vec3.squaredDistance(
model.moveHandle.getOrigin(),
model.firstHandle.getOrigin()
) <
model.moveHandle.getScale1() * model.moveHandle.getScale1()
) {
model.lastHandle.setVisible(true);
publicAPI.invokeEndInteractionEvent();

if (publicAPI.getResetAfterPointPlacement()) {
publicAPI.reset();
} else {
publicAPI.loseFocus();
}
}
}

model._interactor.render();
}
} else if (model.activeState !== model.moveHandle) {
model.widgetState.deactivate();
}

model.freeHand = false;
model.draggedPoint = false;
model._isDragging = false;

return model.hasFocus ? macro.EVENT_ABORT : macro.VOID;
};

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

publicAPI.handleMouseMove = (callData) => {
const manipulator =
model.activeState?.getManipulator?.() ?? model.manipulator;
if (
!manipulator ||
!model.activeState ||
!model.activeState.getActive() ||
!model.pickable
) {
return macro.VOID;
}
const { worldCoords, worldDelta } = manipulator.handleEvent(
callData,
model._apiSpecificRenderWindow
);

const hoveredHandle = getHoveredHandle();
if (hoveredHandle) {
model.moveHandle.setVisible(false);
if (hoveredHandle !== model.firstHandle) {
model._apiSpecificRenderWindow.setCursor('grabbing');
}
} else if (!model._isDragging && model.hasFocus) {
model.moveHandle.setVisible(true);
model._apiSpecificRenderWindow.setCursor(model.defaultCursor);
}

if (model.lastHandle) {
model.lastHandle.setVisible(true);
}

const isHandleMoving =
model._isDragging || model.activeState === model.moveHandle;

if (worldCoords.length && isHandleMoving) {
const curOrigin = model.activeState.getOrigin();
if (curOrigin) {
model.activeState.setOrigin(add(curOrigin, worldDelta, []));
} else {
model.activeState.setOrigin(worldCoords);
}
if (model._isDragging) {
model.draggedPoint = true;
}
if (model.freeHand && model.activeState === model.moveHandle) {
addPoint();
}
}

return model.hasFocus ? macro.EVENT_ABORT : macro.VOID;
};

// --------------------------------------------------------------------------
// Mofifier keys
// --------------------------------------------------------------------------

publicAPI.handleKeyDown = ({ key }) => {
model.keysDown[key] = true;

if (!model.hasFocus) {
return;
}

if (key === 'Enter') {
if (model.widgetState.getHandleList().length > 0) {
publicAPI.invokeEndInteractionEvent();

if (publicAPI.getResetAfterPointPlacement()) {
publicAPI.reset();
} else {
publicAPI.loseFocus();
}
}
} else if (key === 'Escape') {
publicAPI.reset();
publicAPI.loseFocus();
publicAPI.invokeEndInteractionEvent();
} else if (key === 'Delete' || key === 'Backspace') {
if (model.lastHandle) {
model.widgetState.removeHandle(model.lastHandle);

const handleList = model.widgetState.getHandleList();
model.lastHandle = handleList[handleList.length - 1];
}
}
};

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

publicAPI.handleKeyUp = ({ key }) => {
model.keysDown[key] = false;
};

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

publicAPI.grabFocus = () => {
if (!model.hasFocus) {
model.activeState = model.moveHandle;
model.activeState.activate();
model.activeState.setVisible(true);
model._interactor.requestAnimation(publicAPI);
updateHandlesSize();
}

model.hasFocus = true;
};

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

publicAPI.loseFocus = () => {
if (model.hasFocus) {
model._interactor.cancelAnimation(publicAPI);
}

model.widgetState.deactivate();
model.moveHandle.deactivate();
model.moveHandle.setVisible(false);
model.activeState = null;
model._interactor.render();

model.hasFocus = false;
};
}
index.js
import macro from 'vtk.js/Sources/macros';
import vtkAbstractWidgetFactory from 'vtk.js/Sources/Widgets/Core/AbstractWidgetFactory';
import vtkPlanePointManipulator from 'vtk.js/Sources/Widgets/Manipulators/PlaneManipulator';
import vtkSplineContextRepresentation from 'vtk.js/Sources/Widgets/Representations/SplineContextRepresentation';
import vtkSphereHandleRepresentation from 'vtk.js/Sources/Widgets/Representations/SphereHandleRepresentation';

import widgetBehavior from 'vtk.js/Sources/Widgets/Widgets3D/SplineWidget/behavior';
import stateGenerator from 'vtk.js/Sources/Widgets/Widgets3D/SplineWidget/state';

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

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

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

const superClass = { ...publicAPI };

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

model.methodsToLink = [
'boundaryCondition',
'close',
'outputBorder',
'fill',
'borderColor',
'errorBorderColor',
'scaleInPixels',
];

publicAPI.getRepresentationsForViewType = (viewType) => {
switch (viewType) {
case ViewTypes.DEFAULT:
case ViewTypes.GEOMETRY:
case ViewTypes.SLICE:
case ViewTypes.VOLUME:
default:
return [
{
builder: vtkSphereHandleRepresentation,
labels: ['handles', 'moveHandle'],
},
{
builder: vtkSplineContextRepresentation,
labels: ['handles', 'moveHandle'],
},
];
}
};

// --- Public methods -------------------------------------------------------
publicAPI.setManipulator = (manipulator) => {
superClass.setManipulator(manipulator);
model.widgetState.getMoveHandle().setManipulator(manipulator);
model.widgetState.getHandleList().forEach((handle) => {
handle.setManipulator(manipulator);
});
};

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

// Default manipulator
publicAPI.setManipulator(
model.manipulator ||
model.manipulator ||
vtkPlanePointManipulator.newInstance({ useCameraNormal: true })
);
}

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

const defaultValues = (initialValues) => ({
// manipulator: null,
freehandMinDistance: 0.1,
allowFreehand: true,
resolution: 32, // propagates to SplineContextRepresentation
defaultCursor: 'pointer',
handleSizeInPixels: 10, // propagates to SplineContextRepresentation
resetAfterPointPlacement: false,
behavior: widgetBehavior,
widgetState: stateGenerator(),
...initialValues,
});

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

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

vtkAbstractWidgetFactory.extend(publicAPI, model, initialValues);
macro.setGet(publicAPI, model, [
'manipulator',
'freehandMinDistance',
'allowFreehand',
'resolution',
'defaultCursor',
'handleSizeInPixels',
'resetAfterPointPlacement',
]);

vtkSplineWidget(publicAPI, model);
}

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

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

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

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

import { splineKind } from 'vtk.js/Sources/Common/DataModel/Spline3D/Constants';

import { BoundaryCondition } from 'vtk.js/Sources/Common/DataModel/Spline1D/Constants';

export default function generateState() {
return vtkStateBuilder
.createBuilder()
.addField({ name: 'splineKind', initialValue: splineKind.KOCHANEK_SPLINE })
.addField({ name: 'splineClosed', initialValue: true })
.addField({
name: 'splineBoundaryCondition',
initialValue: BoundaryCondition.DEFAULT,
})
.addField({
name: 'splineBoundaryConditionValues',
initialValue: [0, 0, 0],
})
.addField({ name: 'splineTension', initialValue: 0 })
.addField({ name: 'splineContinuity', initialValue: 0 })
.addField({ name: 'splineBias', initialValue: 0 })
.addStateFromMixin({
labels: ['moveHandle'],
mixins: ['origin', 'color', 'scale1', 'visible', 'manipulator'],
name: 'moveHandle',
initialValues: {
scale1: 10,
visible: false,
},
})
.addDynamicMixinState({
labels: ['handles'],
mixins: ['origin', 'color', 'scale1', 'visible', 'manipulator'],
name: 'handle',
initialValues: {
scale1: 10,
},
})
.build();
}