SphereWidget

Methods

Source

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

export default function widgetBehavior(publicAPI, model) {
const state = model.widgetState;
const moveHandle = state.getMoveHandle();
const centerHandle = state.getCenterHandle();
const borderHandle = state.getBorderHandle();
const shapeHandle = state.getSphereHandle();

// Set while moving the center or border handle.
model._isDragging = false;
// The last world coordinate of the mouse cursor during dragging.
model.previousPosition = null;

model.classHierarchy.push('vtkSphereWidgetProp');

moveHandle.setVisible(true);
centerHandle.setVisible(false);
borderHandle.setVisible(false);
shapeHandle.setVisible(true);

function isValidHandle(handle) {
return (
handle === centerHandle ||
handle === borderHandle ||
handle === moveHandle
);
}

function isPlaced() {
return !!centerHandle.getOrigin() && !!borderHandle.getOrigin();
}

// Update the sphereHandle parameters from {center,border}Handle.
function updateSphere() {
const center = centerHandle.getOrigin();
if (!center) return;

centerHandle.setVisible(true);
let border = borderHandle.getOrigin();
if (border) {
borderHandle.setVisible(true);
} else {
border = moveHandle.getOrigin();
if (!border) return;
}
if (isPlaced()) {
moveHandle.setVisible(false);
}
const radius = vec3.distance(center, border);
shapeHandle.setVisible(true);
shapeHandle.setOrigin(center);
shapeHandle.setScale1(radius * 2);
model._interactor.render();
}

function currentWorldCoords(e) {
const manipulator =
model.activeState?.getManipulator?.() ?? model.manipulator;
return manipulator.handleEvent(e, model._apiSpecificRenderWindow)
.worldCoords;
}

// Update the sphere's center and radius. Example:
// handle.setCenterAndRadius([1,2,3], 10);
publicAPI.setCenterAndRadius = (newCenter, newRadius) => {
const oldCenter = centerHandle.getOrigin();
const oldBorder = borderHandle.getOrigin();
let newBorder = [newCenter[0] + newRadius, newCenter[1], newCenter[2]];
if (oldBorder) {
// Move the boundary handle to reflect the new radius, while preserving
// its direction relative to the center.
const direction = vec3.sub(vec3.create(), oldBorder, oldCenter);
const oldRadius = vec3.length(direction);
if (oldRadius > 1e-10) {
newBorder = vec3.add(
vec3.create(),
newCenter,
vec3.scale(vec3.create(), direction, newRadius / oldRadius)
);
}
}
centerHandle.setOrigin(newCenter);
borderHandle.setOrigin(newBorder);
updateSphere();
model._widgetManager.enablePicking();
};

publicAPI.handleLeftButtonPress = (e) => {
if (!isValidHandle(model.activeState)) {
model.activeState = null;
return macro.VOID;
}
const worldCoords = currentWorldCoords(e);

if (model.activeState === moveHandle) {
// Initial sphere placement.
if (!centerHandle.getOrigin()) {
centerHandle.setOrigin(worldCoords);
} else if (!borderHandle.getOrigin()) {
borderHandle.setOrigin(worldCoords);
}
updateSphere();
}
model._isDragging = true;
model._apiSpecificRenderWindow.setCursor('grabbing');
model.previousPosition = [...currentWorldCoords(e)];
publicAPI.invokeStartInteractionEvent();
return macro.EVENT_ABORT;
};

publicAPI.handleLeftButtonRelease = (e) => {
if (!model._isDragging) {
model.activeState = null;
return macro.VOID;
}
if (isPlaced()) {
model.previousPosition = null;
model._widgetManager.enablePicking();
model._apiSpecificRenderWindow.setCursor('pointer');
model._isDragging = false;
model.activeState = null;
state.deactivate();
}
publicAPI.invokeEndInteractionEvent();
return macro.EVENT_ABORT;
};

publicAPI.handleMouseMove = (e) => {
if (!model._isDragging) {
model.activeState = null;
return macro.VOID;
}
if (!model.activeState) throw Error('no activestate');
const worldCoords = currentWorldCoords(e);
model.activeState.setOrigin(worldCoords);
if (model.activeState === centerHandle) {
// When the sphere is fully placed, and the user is moving the
// center, we move the whole sphere.
if (borderHandle.getOrigin()) {
if (!model.previousPosition) {
// !previousPosition here happens only immediately
// after grabFocus, but grabFocus resets
// borderHandle.origin.
throw Error(`no pos ${model.activeState} ${model.previousPosition}`);
}
const translation = vec3.sub(
vec3.create(),
worldCoords,
model.previousPosition
);
borderHandle.setOrigin(
vec3.add(vec3.create(), borderHandle.getOrigin(), translation)
);
}
}
model.previousPosition = worldCoords;
updateSphere();
return macro.VOID;
};

publicAPI.grabFocus = () => {
moveHandle.setVisible(true);
centerHandle.setVisible(false);
borderHandle.setVisible(false);
centerHandle.setOrigin(null);
borderHandle.setOrigin(null);
model._isDragging = true;
model.activeState = moveHandle;
model._interactor.render();
};

publicAPI.loseFocus = () => {
model._isDragging = false;
model.activeState = null;
};
}
index.d.ts
import vtkAbstractWidget from '../../Core/AbstractWidget';
import { Vector3, Bounds } from '../../../types';

export interface ISphereWidgetHandleState {
getOrigin(): Vector3;
setOrigin(arg: Vector3): void;
getColor(): string;
setColor(arg: string):void;
getScale1(): number;
setScale1(arg: number): void;
getVisible(): boolean;
setVisible(arg: boolean):void
setShape(arg: string): void;
getShape(): string;
}

// The internal state of the widget.
export interface vtkSphereWidgetState {
// A handle that defines the center of the sphere.
getCenterHandle(): ISphereWidgetHandleState;
// An arbitrary point at the sphere border. Used only to set the radius.
getBorderHandle(): ISphereWidgetHandleState;
}

// The type of object returned by vtkWidgetManager.addWidget()
export interface vtkSphereWidgetHandle {
// Set the sphere parameters.
setCenterAndRadius(center: Vector3, radius: number): void;
}

export interface vtkSphereWidget {
// Abstract widget methods.
getWidgetState(): vtkSphereWidgetState;
onWidgetChange(fn: () => void): void;
placeWidget(bounds: Bounds): void;
setPlaceFactor(factor: number): void;

// Methods specific to vtkSphereWidget.
getRadius(): number;
}

export interface ISphereWidgetInitialValues {}

export function newInstance(props?: ISphereWidgetInitialValues): vtkSphereWidget;

export const vtkSphereWidget: {
newInstance: typeof newInstance;
};

export default vtkSphereWidget;
index.js
import { distance2BetweenPoints } from 'vtk.js/Sources/Common/Core/Math';
import vtkAbstractWidgetFactory from 'vtk.js/Sources/Widgets/Core/AbstractWidgetFactory';
import vtkPlanePointManipulator from 'vtk.js/Sources/Widgets/Manipulators/PlaneManipulator';
import vtkSphereHandleRepresentation from 'vtk.js/Sources/Widgets/Representations/SphereHandleRepresentation';
import vtkSphereContextRepresentation from 'vtk.js/Sources/Widgets/Representations/SphereContextRepresentation';
import macro from 'vtk.js/Sources/macros';

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

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

const superClass = { ...publicAPI };

model.methodsToLink = ['scaleInPixels'];

publicAPI.getRepresentationsForViewType = (viewType) => [
{
builder: vtkSphereHandleRepresentation,
labels: ['moveHandle'],
},
{
builder: vtkSphereHandleRepresentation,
labels: ['centerHandle'],
},
{
builder: vtkSphereHandleRepresentation,
labels: ['borderHandle'],
},
{
builder: vtkSphereContextRepresentation,
labels: ['sphereHandle'],
},
];

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

publicAPI.getRadius = () => {
const h1 = model.widgetState.getCenterHandle();
const h2 = model.widgetState.getBorderHandle();
return Math.sqrt(distance2BetweenPoints(h1.getOrigin(), h2.getOrigin()));
};

publicAPI.setManipulator = (manipulator) => {
superClass.setManipulator(manipulator);
model.widgetState.getMoveHandle().setManipulator(manipulator);
model.widgetState.getCenterHandle().setManipulator(manipulator);
model.widgetState.getBorderHandle().setManipulator(manipulator);
};

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

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

const defaultValues = (initialValues) => ({
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', 'widgetState']);
vtkSphereWidget(publicAPI, model);
}

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

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

// Defines the structure of the widget state.
// See https://kitware.github.io/vtk-js/docs/concepts_widgets.html.
export default function stateGenerator() {
return (
vtkStateBuilder
.createBuilder()
// The handle used only for during initial placement.
.addStateFromMixin({
labels: ['moveHandle'],
mixins: ['origin', 'color', 'scale1', 'visible', 'manipulator'],
name: 'moveHandle',
initialValues: {
scale1: 20,
visible: true,
},
})
// The handle for the center of the sphere.
.addStateFromMixin({
labels: ['centerHandle'],
mixins: ['origin', 'color', 'scale1', 'visible', 'manipulator'],
name: 'centerHandle',
initialValues: {
scale1: 20,
visible: true,
},
})
// The handle for a border point of the sphere.
.addStateFromMixin({
labels: ['borderHandle'],
mixins: ['origin', 'color', 'scale1', 'visible', 'manipulator'],
name: 'borderHandle',
initialValues: {
scale1: 20,
visible: true,
},
})
// For displaying the sphere.
.addStateFromMixin({
labels: ['sphereHandle'],
mixins: ['origin', 'color', 'scale1', 'visible', 'orientation'],
name: 'sphereHandle',
initialValues: {
visible: true,
radius: 1,
},
})
.build()
);
}