diff --git a/Documentation/content/examples/index.md b/Documentation/content/examples/index.md index b355c30af8c..689339c6d14 100644 --- a/Documentation/content/examples/index.md +++ b/Documentation/content/examples/index.md @@ -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") diff --git a/Sources/Widgets/Representations/LineGlyphRepresentation/example/index.js b/Sources/Widgets/Representations/LineGlyphRepresentation/example/index.js new file mode 100644 index 00000000000..02d94552076 --- /dev/null +++ b/Sources/Widgets/Representations/LineGlyphRepresentation/example/index.js @@ -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(); diff --git a/Sources/Widgets/Representations/LineGlyphRepresentation/index.js b/Sources/Widgets/Representations/LineGlyphRepresentation/index.js new file mode 100644 index 00000000000..b032d6de732 --- /dev/null +++ b/Sources/Widgets/Representations/LineGlyphRepresentation/index.js @@ -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 }; diff --git a/Sources/Widgets/Representations/WidgetRepresentation/index.d.ts b/Sources/Widgets/Representations/WidgetRepresentation/index.d.ts index 0370c30d76a..363ee6e80cd 100644 --- a/Sources/Widgets/Representations/WidgetRepresentation/index.d.ts +++ b/Sources/Widgets/Representations/WidgetRepresentation/index.d.ts @@ -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, - coincidentTopologyParameters?: object, - displayScaleParams?: IDisplayScaleParams, - scaleInPixels?: boolean + labels?: Array; + coincidentTopologyParameters?: object; + displayScaleParams?: IDisplayScaleParams; + scaleInPixels?: boolean; } export interface vtkWidgetRepresentation extends vtkProp { - getLabels(): Array; - setLabels(labels: Array): void; + getLabels(): Array; + setLabels(labels: Array): 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; } /** @@ -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`. @@ -92,7 +100,6 @@ 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') @@ -100,14 +107,16 @@ export function connectPipeline(pipeline: IWidgetPipeline): void; * @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;