diff --git a/Sources/Filters/Core/PolyDataNormals/example/controlPanel.html b/Sources/Filters/Core/PolyDataNormals/example/controlPanel.html new file mode 100644 index 00000000000..d5b0ca6793f --- /dev/null +++ b/Sources/Filters/Core/PolyDataNormals/example/controlPanel.html @@ -0,0 +1,14 @@ + + + + + + + + + +
Compute point normals + +
Compute cell normals + +
diff --git a/Sources/Filters/Core/PolyDataNormals/example/index.js b/Sources/Filters/Core/PolyDataNormals/example/index.js new file mode 100644 index 00000000000..75256b35463 --- /dev/null +++ b/Sources/Filters/Core/PolyDataNormals/example/index.js @@ -0,0 +1,112 @@ +import '@kitware/vtk.js/favicon'; + +// Load the rendering pieces we want to use (for both WebGL and WebGPU) +import '@kitware/vtk.js/Rendering/Profiles/Geometry'; +import '@kitware/vtk.js/Rendering/Profiles/Glyph'; + +import vtkFullScreenRenderWindow from '@kitware/vtk.js/Rendering/Misc/FullScreenRenderWindow'; + +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 vtkLookupTable from '@kitware/vtk.js/Common/Core/LookupTable'; +import vtkGlyph3DMapper from '@kitware/vtk.js/Rendering/Core/Glyph3DMapper'; +import vtkMapper from '@kitware/vtk.js/Rendering/Core/Mapper'; +import vtkPolyDataNormals from '@kitware/vtk.js/Filters/Core/PolyDataNormals'; + +import controlPanel from './controlPanel.html'; + +const { ColorMode, ScalarMode } = vtkMapper; + +// ---------------------------------------------------------------------------- +// Standard rendering code setup +// ---------------------------------------------------------------------------- + +const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({ + background: [0.9, 0.9, 0.9], +}); +const renderer = fullScreenRenderer.getRenderer(); +const renderWindow = fullScreenRenderer.getRenderWindow(); + +// ---------------------------------------------------------------------------- +// Example code +// ---------------------------------------------------------------------------- + +const lookupTable = vtkLookupTable.newInstance({ hueRange: [0.666, 0] }); + +const source = vtkCubeSource.newInstance(); +const inputPolyData = source.getOutputData(); +inputPolyData.getPointData().setNormals(null); + +const mapper = vtkMapper.newInstance({ + interpolateScalarsBeforeMapping: true, + colorMode: ColorMode.DEFAULT, + scalarMode: ScalarMode.DEFAULT, + useLookupTableScalarRange: true, + lookupTable, +}); +const actor = vtkActor.newInstance(); +actor.getProperty().setEdgeVisibility(true); + +const polyDataNormals = vtkPolyDataNormals.newInstance(); + +// The generated 'z' array will become the default scalars, so the plane mapper will color by 'z': +polyDataNormals.setInputData(inputPolyData); + +mapper.setInputConnection(polyDataNormals.getOutputPort()); +actor.setMapper(mapper); + +renderer.addActor(actor); + +const arrowSource = vtkArrowSource.newInstance(); + +const glyphMapper = vtkGlyph3DMapper.newInstance(); +glyphMapper.setInputConnection(polyDataNormals.getOutputPort()); +glyphMapper.setSourceConnection(arrowSource.getOutputPort()); +glyphMapper.setOrientationModeToDirection(); +glyphMapper.setOrientationArray('Normals'); +glyphMapper.setScaleModeToScaleByMagnitude(); +glyphMapper.setScaleArray('Normals'); +glyphMapper.setScaleFactor(0.1); + +const glyphActor = vtkActor.newInstance(); +glyphActor.setMapper(glyphMapper); +renderer.addActor(glyphActor); + +renderer.resetCamera(); +renderWindow.render(); + +// ---------------------------------------------------------------------------- +// UI control handling +// ---------------------------------------------------------------------------- + +fullScreenRenderer.addController(controlPanel); + +// Checkbox +document + .querySelector('.computePointNormals') + .addEventListener('change', (e) => { + polyDataNormals.setComputePointNormals(!!e.target.checked); + renderWindow.render(); + }); + +document + .querySelector('.computeCellNormals') + .addEventListener('change', (e) => { + polyDataNormals.setComputeCellNormals(!!e.target.checked); + renderWindow.render(); + }); + +// ----------------------------------------------------------- +// Make some variables global so that you can inspect and +// modify objects in your browser's developer console: +// ----------------------------------------------------------- + +global.mapper = mapper; +global.actor = actor; +global.source = source; +global.renderer = renderer; +global.renderWindow = renderWindow; +global.lookupTable = lookupTable; +global.polyDataNormals = polyDataNormals; +global.glyphMapper = glyphMapper; diff --git a/Sources/Filters/Core/PolyDataNormals/index.js b/Sources/Filters/Core/PolyDataNormals/index.js index 4086b567bf8..3e5a4f0b8df 100644 --- a/Sources/Filters/Core/PolyDataNormals/index.js +++ b/Sources/Filters/Core/PolyDataNormals/index.js @@ -13,17 +13,24 @@ function vtkPolyDataNormals(publicAPI, model) { // Set our className model.classHierarchy.push('vtkPolyDataNormals'); - publicAPI.vtkPolyDataNormalsExecute = (pointsData, polysData) => { + publicAPI.vtkPolyDataNormalsExecute = ( + numberOfPolys, + polysData, + pointsData + ) => { if (!pointsData) { return null; } - const normalsData = new Float32Array(pointsData.length); + const pointNormals = new Float32Array(pointsData.length); + const cellNormals = new Float32Array(3 * numberOfPolys); + let cellNormalComponent = 0; let numberOfPoints = 0; const polysDataLength = polysData.length; const cellPointIds = [0, 0, 0]; + const cellNormal = [0, 0, 0]; for (let c = 0; c < polysDataLength; c += numberOfPoints + 1) { numberOfPoints = polysData[c]; @@ -36,8 +43,6 @@ function vtkPolyDataNormals(publicAPI, model) { cellPointIds[i - 1] = 3 * polysData[c + i]; } - const cellNormal = []; - vtkTriangle.computeNormal( pointsData.slice(cellPointIds[0], cellPointIds[0] + 3), pointsData.slice(cellPointIds[1], cellPointIds[1] + 3), @@ -45,28 +50,39 @@ function vtkPolyDataNormals(publicAPI, model) { cellNormal ); - for (let i = 1; i <= numberOfPoints; ++i) { - let pointId = 3 * polysData[c + i]; + cellNormals[cellNormalComponent++] = cellNormal[0]; + cellNormals[cellNormalComponent++] = cellNormal[1]; + cellNormals[cellNormalComponent++] = cellNormal[2]; + + if (model.computePointNormals) { + for (let i = 1; i <= numberOfPoints; ++i) { + let pointId = 3 * polysData[c + i]; - normalsData[pointId] += cellNormal[0]; - normalsData[++pointId] += cellNormal[1]; - normalsData[++pointId] += cellNormal[2]; + pointNormals[pointId] += cellNormal[0]; + pointNormals[++pointId] += cellNormal[1]; + pointNormals[++pointId] += cellNormal[2]; + } } } - /* Normalize normals */ + // Normalize point normals. + // A point normal is the sum of all the cell normals the point belongs to + if (model.computePointNormals) { + const pointNormal = [0, 0, 0]; + for (let i = 0; i < pointsData.length; ) { + pointNormal[0] = pointNormals[i]; + pointNormal[1] = pointNormals[i + 1]; + pointNormal[2] = pointNormals[i + 2]; - for (let i = 0; i < pointsData.length; ) { - const pointNormal = normalsData.slice(i, i + 3); + vtkMath.normalize(pointNormal); - vtkMath.normalize(pointNormal); - - normalsData[i++] = pointNormal[0]; - normalsData[i++] = pointNormal[1]; - normalsData[i++] = pointNormal[2]; + pointNormals[i++] = pointNormal[0]; + pointNormals[i++] = pointNormal[1]; + pointNormals[i++] = pointNormal[2]; + } } - return normalsData; + return [cellNormals, pointNormals]; }; publicAPI.requestData = (inData, outData) => { @@ -82,18 +98,8 @@ function vtkPolyDataNormals(publicAPI, model) { return; } - const outputNormalsData = publicAPI.vtkPolyDataNormalsExecute( - input.getPoints().getData(), - input.getPolys().getData() - ); - const output = vtkPolyData.newInstance(); - const outputNormals = vtkDataArray.newInstance({ - numberOfComponents: 3, - values: outputNormalsData, - }); - output.setPoints(input.getPoints()); output.setVerts(input.getVerts()); output.setLines(input.getLines()); @@ -104,7 +110,29 @@ function vtkPolyDataNormals(publicAPI, model) { output.getCellData().passData(input.getCellData()); output.getFieldData().passData(input.getFieldData()); - output.getPointData().setNormals(outputNormals); + const [cellNormals, pointNormals] = publicAPI.vtkPolyDataNormalsExecute( + input.getNumberOfPolys(), + input.getPolys().getData(), + input.getPoints().getData() + ); + + if (model.computePointNormals) { + const outputPointNormals = vtkDataArray.newInstance({ + numberOfComponents: 3, + name: 'Normals', + values: pointNormals, + }); + output.getPointData().setNormals(outputPointNormals); + } + + if (model.computeCellNormals) { + const outputCellNormals = vtkDataArray.newInstance({ + numberOfComponents: 3, + name: 'Normals', + values: cellNormals, + }); + output.getCellData().setNormals(outputCellNormals); + } outData[0] = output; }; @@ -115,6 +143,8 @@ function vtkPolyDataNormals(publicAPI, model) { // ---------------------------------------------------------------------------- function defaultValues(initialValues) { return { + computeCellNormals: false, + computePointNormals: true, ...initialValues, }; } @@ -131,6 +161,8 @@ export function extend(publicAPI, model, initialValues = {}) { macro.algo(publicAPI, model, 1, 1); + macro.setGet(publicAPI, model, ['computeCellNormals', 'computePointNormals']); + /* Object specific methods */ vtkPolyDataNormals(publicAPI, model); diff --git a/Sources/Filters/Core/PolyDataNormals/test/testPolyDataNormals.js b/Sources/Filters/Core/PolyDataNormals/test/testPolyDataNormals.js index 9106e88fd50..d1e6adcfd88 100644 --- a/Sources/Filters/Core/PolyDataNormals/test/testPolyDataNormals.js +++ b/Sources/Filters/Core/PolyDataNormals/test/testPolyDataNormals.js @@ -1,7 +1,11 @@ import test from 'tape-catch'; import vtkCubeSource from 'vtk.js/Sources/Filters/Sources/CubeSource'; +import vtkMath from 'vtk.js/Sources/Common/Core/Math'; import vtkPolyDataNormals from 'vtk.js/Sources/Filters/Core/PolyDataNormals'; +import vtkTriangle from 'vtk.js/Sources/Common/DataModel/Triangle'; + +const PRECISION = 4; test('Test vtkPolyDataNormals passData', (t) => { const cube = vtkCubeSource.newInstance(); @@ -24,3 +28,70 @@ test('Test vtkPolyDataNormals passData', (t) => { t.end(); }); + +test('Test vtkPolyDataNormals normals', (t) => { + const cube = vtkCubeSource.newInstance(); + const input = cube.getOutputData(); + const pointNormalsData = input.getPointData().getNormals().getData(); + // const cellNormalsData = input.getCellData().getNormals().getData(); + input.getPointData().setNormals(null); + input.getCellData().setNormals(null); + + const normals = vtkPolyDataNormals.newInstance(); + normals.setInputData(input); + normals.setComputeCellNormals(true); + normals.update(); + const output = normals.getOutputData(); + + console.log(pointNormalsData); + console.log(output.getPointData().getNormals().getData()); + t.deepEqual( + vtkMath.roundVector(pointNormalsData, [], PRECISION), + vtkMath.roundVector( + output.getPointData().getNormals().getData(), + [], + PRECISION + ), + 'Same point normals' + ); + + const pointsData = output.getPoints().getData(); + const polysData = output.getPolys().getData(); + const polysDataLength = polysData.length; + const cellPointIds = [0, 0, 0]; + let numberOfPoints = 0; + let polysId = 0; + for (let c = 0; c < polysDataLength; c += numberOfPoints + 1) { + numberOfPoints = polysData[c]; + + for (let i = 1; i <= 3; ++i) { + cellPointIds[i - 1] = 3 * polysData[c + i]; + } + + const cellNormal = []; + + vtkTriangle.computeNormal( + pointsData.slice(cellPointIds[0], cellPointIds[0] + 3), + pointsData.slice(cellPointIds[1], cellPointIds[1] + 3), + pointsData.slice(cellPointIds[2], cellPointIds[2] + 3), + cellNormal + ); + + t.deepEqual( + vtkMath.roundVector(cellNormal, [], PRECISION), + vtkMath.roundVector( + output + .getCellData() + .getNormals() + .getData() + .slice(3 * polysId, 3 * polysId + 3), + [], + PRECISION + ), + `Same cell normal #${polysId}` + ); + ++polysId; + } + + t.end(); +}); diff --git a/Sources/Rendering/Core/Glyph3DMapper/index.d.ts b/Sources/Rendering/Core/Glyph3DMapper/index.d.ts index c4a47862838..6281beb9fc7 100755 --- a/Sources/Rendering/Core/Glyph3DMapper/index.d.ts +++ b/Sources/Rendering/Core/Glyph3DMapper/index.d.ts @@ -1,4 +1,4 @@ -import { Bounds } from "../../../types"; +import { Bounds, Nullable, vtkPipelineConnection } from "../../../types"; import vtkMapper, { IMapperInitialValues } from "../Mapper"; import { OrientationModes, ScaleModes } from "./Constants"; @@ -87,6 +87,12 @@ export interface vtkGlyph3DMapper extends vtkMapper { */ getPrimitiveCount(): IPrimitiveCount; + /** + * Sets the name of the array to use as orientation. + * @param {String} arrayName Name of the array + */ + setOrientationArray(arrayName: Nullable): boolean; + /** * Orientation mode indicates if the OrientationArray provides the direction * vector for the orientation or the rotations around each axes. @@ -138,6 +144,13 @@ export interface vtkGlyph3DMapper extends vtkMapper { * Set scale to `SCALE_BY_CONSTANT` */ setScaleModeToScaleByConstant(): boolean; + + /** + * Convenient method to set the source glyph connection + * @param {vtkPipelineConnection} outputPort The output port of the glyph source. + */ + setSourceConnection(outputPort: vtkPipelineConnection): void; + } /** diff --git a/Sources/Rendering/Core/Glyph3DMapper/index.js b/Sources/Rendering/Core/Glyph3DMapper/index.js index 4612ae80c33..a3d9ac232f6 100644 --- a/Sources/Rendering/Core/Glyph3DMapper/index.js +++ b/Sources/Rendering/Core/Glyph3DMapper/index.js @@ -134,6 +134,7 @@ function vtkGlyph3DMapper(publicAPI, model) { model.normalArray = new Float32Array(9 * numPts); const nbuff = model.normalArray.buffer; const tuple = []; + const orientation = []; for (let i = 0; i < numPts; ++i) { const z = new Float32Array(mbuff, i * 64, 16); trans[0] = pts[i * 3]; @@ -142,7 +143,6 @@ function vtkGlyph3DMapper(publicAPI, model) { mat4.translate(z, identity, trans); if (oArray) { - const orientation = []; oArray.getTuple(i, orientation); switch (model.orientationMode) { case OrientationModes.MATRIX: { @@ -299,6 +299,9 @@ function vtkGlyph3DMapper(publicAPI, model) { }; return pcount; }; + + publicAPI.setSourceConnection = (outputPort) => + publicAPI.setInputConnection(outputPort, 1); } // ----------------------------------------------------------------------------