Skip to content

LineGlyphRepresentation to connect widget handles #2899

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Documentation/content/examples/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ This will allow you to see the some live code running in your browser. Just pick
[![ResliceCursorWidget Example][ResliceCursorWidget]](./ResliceCursorWidget.html "Axial Coronal and Sagittal MPR/Oblique/Reformatted/Resliced/Slab/MIP views")
[![ShapeWidget Example][ShapeWidget]](./ShapeWidget.html "2D shape widgets with text information")
[![SphereWidget Example][SphereWidget]](./SphereWidget.html "2D sphere widget controlled with radius")
[![SplineWidget Example][SplineWidget]](./PaintWidget.html "Widget to draw open or closed (triangularized) sharp/smooth polygon widget")
[![SplineWidget Example][SplineWidget]](./SplineWidget.html "Widget to draw open or closed (triangularized) sharp/smooth polygon widget")

</div>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import '@kitware/vtk.js/favicon';

// Load the rendering pieces we want to use (for both WebGL and WebGPU)
import '@kitware/vtk.js/Rendering/Profiles/All';

import vtkFullScreenRenderWindow from '@kitware/vtk.js/Rendering/Misc/FullScreenRenderWindow';
import vtkStateBuilder from '@kitware/vtk.js/Widgets/Core/StateBuilder';

import vtkSphereHandleRepresentation from '@kitware/vtk.js/Widgets/Representations/SphereHandleRepresentation';
import vtkLineGlyphRepresentation from '@kitware/vtk.js/Widgets/Representations/LineGlyphRepresentation';

// ----------------------------------------------------------------------------
// Standard rendering code setup
// ----------------------------------------------------------------------------

const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({
background: [0, 0, 0],
});
const renderer = fullScreenRenderer.getRenderer();
const renderWindow = fullScreenRenderer.getRenderWindow();

// -----------------------------------------------------------
// State
// -----------------------------------------------------------

const compositeState = vtkStateBuilder
.createBuilder()
.addDynamicMixinState({
labels: ['handles'],
mixins: ['origin', 'scale1'],
name: 'handle',
})
.build();

const z = -50;
const points = [
[0, 0, z],
[5, -5, z],
[15, 5, z],
[5, 10, z],
];
points.forEach((point) => {
const handle = compositeState.addHandle();
handle.setOrigin(point);
handle.setScale1(1.5);
});

// -----------------------------------------------------------
// Representation
// -----------------------------------------------------------

const widgetRep = vtkLineGlyphRepresentation.newInstance({
scaleInPixels: false,
});
widgetRep.setInputData(compositeState);
widgetRep.setLabels(['handles']);
widgetRep.getActors().forEach(renderer.addActor);

const handleRep = vtkSphereHandleRepresentation.newInstance({
scaleInPixels: false,
});
handleRep.setInputData(compositeState);
handleRep.setLabels(['handles']);
handleRep.getActors().forEach(renderer.addActor);

renderer.resetCamera();
renderWindow.render();
149 changes: 149 additions & 0 deletions Sources/Widgets/Representations/LineGlyphRepresentation/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import macro from '@kitware/vtk.js/macros';
import * as vtkMath from '@kitware/vtk.js/Common/Core/Math';
import vtkGlyph3DMapper from '@kitware/vtk.js/Rendering/Core/Glyph3DMapper';
import vtkGlyphRepresentation from '@kitware/vtk.js/Widgets/Representations/GlyphRepresentation';
import { Behavior } from '@kitware/vtk.js/Widgets/Representations/WidgetRepresentation/Constants';
import { allocateArray } from '@kitware/vtk.js/Widgets/Representations/WidgetRepresentation';
import vtkCylinderSource from '@kitware/vtk.js/Filters/Sources/CylinderSource';
import { OrientationModes } from '@kitware/vtk.js/Rendering/Core/Glyph3DMapper/Constants';
import { getPixelWorldHeightAtCoord } from '@kitware/vtk.js/Widgets/Core/WidgetManager';

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

publicAPI.setGlyphResolution = macro.chain(
publicAPI.setGlyphResolution,
model._pipeline.glyph.setResolution
);
}

function cylinderScale(publicAPI, model) {
return (polyData, states) => {
model._pipeline.mapper.setScaleArray('scale');
model._pipeline.mapper.setScaleFactor(1);
model._pipeline.mapper.setScaling(true);
model._pipeline.mapper.setScaleMode(
vtkGlyph3DMapper.ScaleModes.SCALE_BY_COMPONENTS
);
const scales = allocateArray(
polyData,
'scale',
states.length,
'Float32Array',
3
).getData();
let j = 0;
for (let i = 0; i < states.length; ++i) {
const state = states[i];
const origin = state.getOrigin();
const nextOrigin =
states[i === states.length - 1 ? 0 : i + 1].getOrigin();

const direction = vtkMath.subtract(nextOrigin, origin, []);
const length = vtkMath.normalize(direction);

let scaleFactor = state.getActive() ? model.activeScaleFactor : 1;
if (publicAPI.getScaleInPixels()) {
scaleFactor *= getPixelWorldHeightAtCoord(
state.getOrigin(),
model.displayScaleParams
);
}
if (!model.forceLineThickness) {
scaleFactor *= state.getScale1?.() ?? 1;
}
const scale = [1, model.lineThickness, model.lineThickness];
scales[j++] = length * scale[0];
scales[j++] = scaleFactor * scale[1];
scales[j++] = scaleFactor * scale[2];
}
};
}

function cylinderDirection(publicAPI, model) {
return (polyData, states) => {
model._pipeline.mapper.setOrientationArray('orientation');
model._pipeline.mapper.setOrientationMode(OrientationModes.MATRIX);
const orientation = allocateArray(
polyData,
'orientation',
states.length,
'Float32Array',
9
).getData();
for (let i = 0; i < states.length; ++i) {
const state = states[i];
const origin = state.getOrigin();
const nextOrigin =
states[i === states.length - 1 ? 0 : i + 1].getOrigin();

const direction = vtkMath.subtract(nextOrigin, origin, []);
vtkMath.normalize(direction);
const right = [1, 0, 0];
const up = [0, 1, 0];
vtkMath.perpendiculars(direction, up, right, 0);

orientation.set(direction, 9 * i);
orientation.set(up, 9 * i + 3);
orientation.set(right, 9 * i + 6);
}
};
}

// ----------------------------------------------------------------------------
// Object factory
// ----------------------------------------------------------------------------

function defaultValues(publicAPI, model, initialValues) {
return {
behavior: Behavior.CONTEXT,
glyphResolution: 32,
lineThickness: 0.5, // radius of the cylinder
forceLineThickness: false,
...initialValues,
_pipeline: {
glyph:
initialValues?.pipeline?.glyph ??
vtkCylinderSource.newInstance({
direction: [1, 0, 0],
center: [0.5, 0, 0], // origin of cylinder at end, not center
capping: false,
}),
...initialValues?.pipeline,
},
applyMixin: {
noScale: cylinderScale(publicAPI, model),
scale1: cylinderScale(publicAPI, model),
noOrientation: cylinderDirection(publicAPI, model),
},
};
}

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

export function extend(publicAPI, model, initialValues = {}) {
vtkGlyphRepresentation.extend(
publicAPI,
model,
defaultValues(publicAPI, model, initialValues)
);
macro.setGet(publicAPI, model, [
'glyphResolution',
'lineThickness',
'forceLineThickness',
]);
macro.get(publicAPI, model._pipeline, ['glyph', 'mapper', 'actor']);

vtkLineGlyphRepresentation(publicAPI, model);
}

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

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

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

export default { newInstance, extend };
121 changes: 65 additions & 56 deletions Sources/Widgets/Representations/WidgetRepresentation/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,52 @@
import vtkDataArray from "../../../Common/Core/DataArray";
import vtkPolyData from "../../../Common/DataModel/PolyData";
import { vtkObject } from "../../../interfaces";
import vtkProp from "../../../Rendering/Core/Prop";
import vtkDataArray from '../../../Common/Core/DataArray';
import vtkPolyData from '../../../Common/DataModel/PolyData';
import vtkProp from '../../../Rendering/Core/Prop';
export interface IDisplayScaleParams {
dispHeightFactor: number,
cameraPosition: number[],
cameraDir: number[],
isParallel: boolean,
rendererPixelDims: number[],
dispHeightFactor: number;
cameraPosition: number[];
cameraDir: number[];
isParallel: boolean;
rendererPixelDims: number[];
}

export interface IWidgetRepresentationInitialValues {
labels?: Array<any>,
coincidentTopologyParameters?: object,
displayScaleParams?: IDisplayScaleParams,
scaleInPixels?: boolean
labels?: Array<any>;
coincidentTopologyParameters?: object;
displayScaleParams?: IDisplayScaleParams;
scaleInPixels?: boolean;
}

export interface vtkWidgetRepresentation extends vtkProp {
getLabels(): Array<any>;
setLabels(labels: Array<any>): void;
getLabels(): Array<any>;
setLabels(labels: Array<any>): void;

/**
* Gets the coincident topology parameters applied on the actor mappers
*/
getCoincidentTopologyParameters(): object;
/**
* Sets the coincident topology parameters applied on the actor mappers
*/
setCoincidentTopologyParameters(parameters: object): boolean;
/**
* Gets the coincident topology parameters applied on the actor mappers
*/
getCoincidentTopologyParameters(): object;
/**
* Sets the coincident topology parameters applied on the actor mappers
*/
setCoincidentTopologyParameters(parameters: object): boolean;

/**
* Sets the current view and camera scale parameters.
* Called by the WidgetManager.
* @see setScaleInPixels()
*/
setDisplayScaleParams(params: object): boolean;
/**
* Sets the current view and camera scale parameters.
* Called by the WidgetManager.
* @see setScaleInPixels()
*/
setDisplayScaleParams(params: object): boolean;

/**
* Gets wether actors should have a fix size in display coordinates.
* @see setScaleInPixels()
*/
getScaleInPixels(): boolean;
/**
* Gets wether actors should have a fix size in display coordinates.
* @see setScaleInPixels()
*/
getScaleInPixels(): boolean;

/**
* Sets wether actors should have a fix size in display coordinates.
* @see getScaleInPixels()
*/
setScaleInPixels(scale: boolean): boolean;
/**
* Sets wether actors should have a fix size in display coordinates.
* @see getScaleInPixels()
*/
setScaleInPixels(scale: boolean): boolean;
}

/**
Expand All @@ -57,27 +56,36 @@ export interface vtkWidgetRepresentation extends vtkProp {
* @param model object on which data structure will be bounds (protected)
* @param {IWidgetRepresentationInitialValues} [initialValues] (default: {})
*/
export function extend(publicAPI: object, model: object, initialValues?: IWidgetRepresentationInitialValues): void;
export function extend(
publicAPI: object,
model: object,
initialValues?: IWidgetRepresentationInitialValues
): void;

/**
* Method use to create a new instance of vtkWidgetRepresentation
* @param {IWidgetRepresentationInitialValues} [initialValues] for pre-setting some of its content
*/
export function newInstance(initialValues?: IWidgetRepresentationInitialValues): vtkWidgetRepresentation;
export function newInstance(
initialValues?: IWidgetRepresentationInitialValues
): vtkWidgetRepresentation;

/**
* Static function to get the pixel size of a 3D point.
* @param {Number[]} worldCoord 3D point in world coordinates
* @param {IDisplayScaleParams} displayScaleParams Display and camera information
*/
export function getPixelWorldHeightAtCoord(worldCoord: number[], displayScaleParams: IDisplayScaleParams): number[];
export function getPixelWorldHeightAtCoord(
worldCoord: number[],
displayScaleParams: IDisplayScaleParams
): number[];

export interface IWidgetPipeline {
source?: object,
filter?: object,
glyph?: object,
mapper: object,
actor: object
source?: object;
filter?: object;
glyph?: object;
mapper: object;
actor: object;
}
/**
* If provided, connects `source` (dataset or filter) to `filter`.
Expand All @@ -92,22 +100,23 @@ export function connectPipeline(pipeline: IWidgetPipeline): void;
* Allocate or resize a vtkPoint(name='point'), vtkCellArray (name=
* 'line'|'poly') or vtkDataArray (name=any) and add it to the vtkPolyData.
* If allocated, the array is automatically added to the polydata
* Connects mapper to actor.
* @param {vtkPolyData} polyData The polydata to add array to
* @param {string} name The name of the array to add (special handling for
* 'point', 'line, 'poly')
* @param {Number} numberOfTuples The number of tuples to (re)allocate.
* @param {String} dataType The typed array type name.
* @param {Number} numberOfComponents The number of components of the array.
*/
export function allocateArray(polyData: vtkPolyData,
name: string,
numberOfTuples: number,
dataType?: string,
numberOfComponents?: number): vtkDataArray|null;
export function allocateArray(
polyData: vtkPolyData,
name: string,
numberOfTuples: number,
dataType?: string,
numberOfComponents?: number
): vtkDataArray | null;

export declare const vtkWidgetRepresentation: {
newInstance: typeof newInstance;
extend: typeof extend;
}
newInstance: typeof newInstance;
extend: typeof extend;
};
export default vtkWidgetRepresentation;