diff --git a/Documentation/public/gallery/ReverseSense.jpg b/Documentation/public/gallery/ReverseSense.jpg new file mode 100644 index 00000000000..5c7d8c4a2a4 Binary files /dev/null and b/Documentation/public/gallery/ReverseSense.jpg differ diff --git a/Sources/Filters/Core/ReverseSense/example/index.js b/Sources/Filters/Core/ReverseSense/example/index.js new file mode 100644 index 00000000000..f046927e736 --- /dev/null +++ b/Sources/Filters/Core/ReverseSense/example/index.js @@ -0,0 +1,100 @@ +import '@kitware/vtk.js/favicon'; + +import '@kitware/vtk.js/Rendering/Profiles/Geometry'; +import '@kitware/vtk.js/Rendering/Profiles/Glyph'; + +import vtkActor from '@kitware/vtk.js/Rendering/Core/Actor'; +import vtkArrowSource from '@kitware/vtk.js/Filters/Sources/ArrowSource'; +import vtkCubeSource from '@kitware/vtk.js/Filters/Sources/CubeSource'; +import vtkFullScreenRenderWindow from '@kitware/vtk.js/Rendering/Misc/FullScreenRenderWindow'; +import vtkGlyph3DMapper from '@kitware/vtk.js/Rendering/Core/Glyph3DMapper'; +import vtkMapper from '@kitware/vtk.js/Rendering/Core/Mapper'; +import vtkReverseSense from '@kitware/vtk.js/Filters/Core/ReverseSense'; + +import GUI from 'lil-gui'; + +const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance(); +const renderer = fullScreenRenderer.getRenderer(); +const renderWindow = fullScreenRenderer.getRenderWindow(); + +const COLORS = { + source: [0.2, 0.55, 0.86], + reversed: [0.94, 0.52, 0.25], +}; + +function rgb(color) { + return `rgb(${color.map((value) => Math.round(value * 255)).join(', ')})`; +} + +function styleCube(actor, color) { + actor.getProperty().setColor(...color); + actor.getProperty().setDiffuse(0.75); + actor.getProperty().setAmbient(0.2); + actor.getProperty().setSpecular(0.18); + actor.getProperty().setSpecularPower(22); +} + +function addLegend() { + const gui = new GUI({ title: 'Controls' }); + const details = { + source: rgb(COLORS.source), + reversed: rgb(COLORS.reversed), + }; + + gui.addColor(details, 'source').name('Source').disable(); + gui.addColor(details, 'reversed').name('Reversed').disable(); +} + +const cubeSource1 = vtkCubeSource.newInstance(); +const cubeActor1 = vtkActor.newInstance(); +const cubeMapper1 = vtkMapper.newInstance(); +cubeActor1.setMapper(cubeMapper1); +cubeMapper1.setInputConnection(cubeSource1.getOutputPort()); +styleCube(cubeActor1, COLORS.source); +renderer.addActor(cubeActor1); + +const arrowSource1 = vtkArrowSource.newInstance(); +const glyphMapper1 = vtkGlyph3DMapper.newInstance(); +glyphMapper1.setInputConnection(cubeSource1.getOutputPort()); +glyphMapper1.setSourceConnection(arrowSource1.getOutputPort()); +glyphMapper1.setOrientationModeToDirection(); +glyphMapper1.setOrientationArray('Normals'); +glyphMapper1.setScaleModeToScaleByMagnitude(); +glyphMapper1.setScaleArray('Normals'); +glyphMapper1.setScaleFactor(0.1); + +const glyphActor1 = vtkActor.newInstance(); +glyphActor1.setMapper(glyphMapper1); +renderer.addActor(glyphActor1); + +const cubeSource2 = vtkCubeSource.newInstance(); +const cubeActor2 = vtkActor.newInstance(); +const cubeMapper2 = vtkMapper.newInstance(); +cubeActor2.setMapper(cubeMapper2); +cubeMapper2.setInputConnection(cubeSource2.getOutputPort()); +cubeActor2.setPosition(2, 0, 0); +styleCube(cubeActor2, COLORS.reversed); +renderer.addActor(cubeActor2); + +const reverseSense = vtkReverseSense.newInstance({ reverseNormals: true }); +reverseSense.setInputConnection(cubeSource2.getOutputPort()); + +const arrowSource2 = vtkArrowSource.newInstance(); +const glyphMapper2 = vtkGlyph3DMapper.newInstance(); +glyphMapper2.setInputConnection(reverseSense.getOutputPort()); +glyphMapper2.setSourceConnection(arrowSource2.getOutputPort()); +glyphMapper2.setOrientationModeToDirection(); +glyphMapper2.setOrientationArray('Normals'); +glyphMapper2.setScaleModeToScaleByMagnitude(); +glyphMapper2.setScaleArray('Normals'); +glyphMapper2.setScaleFactor(0.1); + +const glyphActor2 = vtkActor.newInstance(); +glyphActor2.setMapper(glyphMapper2); +glyphActor2.setPosition(2, 0, 0); +renderer.addActor(glyphActor2); + +addLegend(); + +renderer.resetCamera(); +renderWindow.render(); diff --git a/Sources/Filters/Core/ReverseSense/index.d.ts b/Sources/Filters/Core/ReverseSense/index.d.ts new file mode 100644 index 00000000000..1060c9d3bde --- /dev/null +++ b/Sources/Filters/Core/ReverseSense/index.d.ts @@ -0,0 +1,72 @@ +import { vtkAlgorithm, vtkObject } from '../../../interfaces'; + +export interface IReverseSenseInitialValues { + reverseCells?: boolean; + reverseNormals?: boolean; +} + +type vtkReverseSenseBase = vtkObject & vtkAlgorithm; + +export interface vtkReverseSense extends vtkReverseSenseBase { + /** + * Get whether the order of polygonal cells is reversed. + */ + getReverseCells(): boolean; + + /** + * Get whether the direction of point and cell normals is reversed. + */ + getReverseNormals(): boolean; + + /** + * Request data from the input and produce output. + * @param inData The input data. + * @param outData The output data. + */ + requestData(inData: any, outData: any): void; + + /** + * Controls whether the order of polygonal cells is reversed. + * @param {Boolean} reverseCells The new state of the `reverseCells` flag. + */ + setReverseCells(reverseCells: boolean): boolean; + + /** + * Controls whether the direction of point and cell normals is reversed. + * @param {Boolean} reverseNormals The new state of the `reverseNormals` flag. + */ + setReverseNormals(reverseNormals: boolean): boolean; +} + +/** + * Method used to decorate a given object (publicAPI+model) with vtkReverseSense characteristics. + * + * @param publicAPI object on which methods will be bounds (public) + * @param model object on which data structure will be bounds (protected) + * @param {IReverseSenseInitialValues} [initialValues] (default: {}) + */ +export function extend( + publicAPI: object, + model: object, + initialValues?: IReverseSenseInitialValues +): void; + +/** + * Method used to create a new instance of vtkReverseSense. + * @param {IReverseSenseInitialValues} [initialValues] for pre-setting some of its content + */ +export function newInstance( + initialValues?: IReverseSenseInitialValues +): vtkReverseSense; + +/** + * vtkReverseSense is a filter that reverses the order of polygonal cells and/or reverses the direction of point and cell normals. + * Two flags are used to control these operations: `reverseCells` and `reverseNormals`. + * Cell reversal means reversing the order of indices in the cell connectivity list. + * Normal reversal means multiplying the normal vector by -1 (both point and cell normals, if present). + */ +export declare const vtkReverseSense: { + newInstance: typeof newInstance; + extend: typeof extend; +}; +export default vtkReverseSense; diff --git a/Sources/Filters/Core/ReverseSense/index.js b/Sources/Filters/Core/ReverseSense/index.js new file mode 100644 index 00000000000..73ea455e9d7 --- /dev/null +++ b/Sources/Filters/Core/ReverseSense/index.js @@ -0,0 +1,142 @@ +import macro from 'vtk.js/Sources/macros'; + +import vtkCellArray from 'vtk.js/Sources/Common/Core/CellArray'; +import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray'; +import vtkPolyData from 'vtk.js/Sources/Common/DataModel/PolyData'; + +const { vtkErrorMacro } = macro; + +function reverseCellArray(inputCells) { + if (!inputCells) { + return null; + } + + const inputData = inputCells.getData(); + const outputData = inputData.slice(); + + for (let offset = 0, len = inputData.length; offset < len; ) { + const cellSize = inputData[offset]; + let left = offset + 1; + let right = offset + cellSize; + // reverse segment + while (left < right) { + const l = inputData[left]; + const r = inputData[right]; + + outputData[left++] = r; + outputData[right--] = l; + } + offset += cellSize + 1; + } + + return vtkCellArray.newInstance({ + values: outputData, + numberOfComponents: 1, + }); +} + +function reverseNormals(inputNormals) { + if (!inputNormals) { + return null; + } + + const values = inputNormals.getData().slice(); + for (let i = 0; i < values.length; i++) { + const v = -values[i]; + values[i] = v === 0 ? 0 : v; // avoids -0 + } + + return vtkDataArray.newInstance({ + name: inputNormals.getName(), + values, + numberOfComponents: inputNormals.getNumberOfComponents(), + }); +} + +// ---------------------------------------------------------------------------- +// vtkReverseSense methods +// ---------------------------------------------------------------------------- + +function vtkReverseSense(publicAPI, model) { + model.classHierarchy.push('vtkReverseSense'); + + publicAPI.requestData = (inData, outData) => { + const input = inData[0]; + + if (!input) { + vtkErrorMacro('No input!'); + return; + } + + const output = outData[0]?.initialize() || vtkPolyData.newInstance(); + + output.setPoints(input.getPoints()); + + output.setVerts( + model.reverseCells ? reverseCellArray(input.getVerts()) : input.getVerts() + ); + output.setLines( + model.reverseCells ? reverseCellArray(input.getLines()) : input.getLines() + ); + output.setPolys( + model.reverseCells ? reverseCellArray(input.getPolys()) : input.getPolys() + ); + output.setStrips( + model.reverseCells + ? reverseCellArray(input.getStrips()) + : input.getStrips() + ); + + const outPointData = output.getPointData(); + const outCellData = output.getCellData(); + + outPointData.passData(input.getPointData()); + outCellData.passData(input.getCellData()); + output.getFieldData().passData(input.getFieldData()); + + if (model.reverseNormals) { + const pointNormals = reverseNormals(input.getPointData().getNormals()); + const cellNormals = reverseNormals(input.getCellData().getNormals()); + + if (pointNormals) { + outPointData.setNormals(pointNormals); + } + + if (cellNormals) { + outCellData.setNormals(cellNormals); + } + } + + outData[0] = output; + }; +} + +// ---------------------------------------------------------------------------- +// Object factory +// ---------------------------------------------------------------------------- + +const DEFAULT_VALUES = { + reverseCells: true, + reverseNormals: false, +}; + +// ---------------------------------------------------------------------------- + +export function extend(publicAPI, model, initialValues = {}) { + Object.assign(model, DEFAULT_VALUES, initialValues); + + macro.obj(publicAPI, model); + macro.algo(publicAPI, model, 1, 1); + + macro.setGet(publicAPI, model, ['reverseCells', 'reverseNormals']); + + vtkReverseSense(publicAPI, model); +} + +// ---------------------------------------------------------------------------- + +export const newInstance = macro.newInstance(extend, 'vtkReverseSense'); + +// ---------------------------------------------------------------------------- + +export default { newInstance, extend }; diff --git a/Sources/Filters/Core/ReverseSense/test/testReverseSense.js b/Sources/Filters/Core/ReverseSense/test/testReverseSense.js new file mode 100644 index 00000000000..1fa4ed799b5 --- /dev/null +++ b/Sources/Filters/Core/ReverseSense/test/testReverseSense.js @@ -0,0 +1,117 @@ +import test from 'tape'; + +import vtkCellArray from 'vtk.js/Sources/Common/Core/CellArray'; +import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray'; +import vtkPoints from 'vtk.js/Sources/Common/Core/Points'; +import vtkPolyData from 'vtk.js/Sources/Common/DataModel/PolyData'; +import vtkReverseSense from 'vtk.js/Sources/Filters/Core/ReverseSense'; + +function createPolyData() { + const points = vtkPoints.newInstance(); + points.setData( + new Float32Array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1]), + 3 + ); + + const verts = vtkCellArray.newInstance(); + verts.insertNextCell([0, 4]); + + const lines = vtkCellArray.newInstance(); + lines.insertNextCell([0, 1, 2]); + + const polys = vtkCellArray.newInstance(); + polys.insertNextCell([0, 1, 2, 3]); + + const strips = vtkCellArray.newInstance(); + strips.insertNextCell([0, 1, 2, 3, 4]); + + const pointNormals = vtkDataArray.newInstance({ + name: 'Normals', + numberOfComponents: 3, + values: new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1]), + }); + + const cellNormals = vtkDataArray.newInstance({ + name: 'Normals', + numberOfComponents: 3, + values: new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1]), + }); + + const polyData = vtkPolyData.newInstance(); + polyData.setPoints(points); + polyData.setVerts(verts); + polyData.setLines(lines); + polyData.setPolys(polys); + polyData.setStrips(strips); + polyData.getPointData().setNormals(pointNormals); + polyData.getCellData().setNormals(cellNormals); + + return polyData; +} + +test('vtkReverseSense reverses cell connectivity and normals', (t) => { + const reverseSense = vtkReverseSense.newInstance({ + reverseCells: true, + reverseNormals: true, + }); + reverseSense.setInputData(createPolyData()); + + const output = reverseSense.getOutputData(); + + t.deepEqual( + Array.from(output.getVerts().getData()), + [2, 4, 0], + 'reversed verts' + ); + t.deepEqual( + Array.from(output.getLines().getData()), + [3, 2, 1, 0], + 'reversed lines' + ); + t.deepEqual( + Array.from(output.getPolys().getData()), + [4, 3, 2, 1, 0], + 'reversed polys' + ); + t.deepEqual( + Array.from(output.getStrips().getData()), + [5, 4, 3, 2, 1, 0], + 'reversed strips' + ); + t.deepEqual( + Array.from(output.getPointData().getNormals().getData()), + [-1, 0, 0, 0, -1, 0, 0, 0, -1, -1, -1, 0, 0, -1, -1], + 'reversed point normals' + ); + t.deepEqual( + Array.from(output.getCellData().getNormals().getData()), + [-1, 0, 0, 0, -1, 0, 0, 0, -1, -1, -1, -1], + 'reversed cell normals' + ); + + t.end(); +}); + +test('vtkReverseSense leaves normals unchanged when disabled', (t) => { + const reverseSense = vtkReverseSense.newInstance({ + reverseCells: true, + reverseNormals: false, + }); + const input = createPolyData(); + reverseSense.setInputData(input); + + const output = reverseSense.getOutputData(); + + t.deepEqual( + Array.from(output.getPointData().getNormals().getData()), + Array.from(input.getPointData().getNormals().getData()), + 'point normals are unchanged' + ); + t.deepEqual( + Array.from(output.getCellData().getNormals().getData()), + Array.from(input.getCellData().getNormals().getData()), + 'cell normals are unchanged' + ); + + t.end(); +}); diff --git a/Sources/Filters/Core/index.js b/Sources/Filters/Core/index.js index fad4539e06c..e02b9c4d757 100644 --- a/Sources/Filters/Core/index.js +++ b/Sources/Filters/Core/index.js @@ -2,6 +2,7 @@ import vtkCleanPolyData from './CleanPolyData'; import vtkClipPolyData from './ClipPolyData'; import vtkCutter from './Cutter'; import vtkPolyDataNormals from './PolyDataNormals'; +import vktReverseSense from './ReverseSense'; import vtkThresholdPoints from './ThresholdPoints'; export default { @@ -9,5 +10,6 @@ export default { vtkClipPolyData, vtkCutter, vtkPolyDataNormals, + vktReverseSense, vtkThresholdPoints, };