diff --git a/Documentation/content/docs/gallery/ThresholdPoints.jpg b/Documentation/content/docs/gallery/ThresholdPoints.jpg new file mode 100644 index 00000000000..f73511b262c Binary files /dev/null and b/Documentation/content/docs/gallery/ThresholdPoints.jpg differ diff --git a/Documentation/content/examples/index.md b/Documentation/content/examples/index.md index 2e4c6ef956e..34dc0ffa1a4 100644 --- a/Documentation/content/examples/index.md +++ b/Documentation/content/examples/index.md @@ -107,6 +107,8 @@ This will allow you to see the some live code running in your browser. Just pick [![TubeFilter Example][TubeFilter]](./TubeFilter.html "TubeFilter") [![Cutter Example][Cutter]](./Cutter.html "Cutter") [![PolyDataNormals Example][PolyDataNormals]](./PolyDataNormals.html "PolyDataNormals") +[![ThresholdPoints Example][ThresholdPoints]](./ThresholdPoints.html "Cut/Treshold points with point data criteria") + @@ -125,6 +127,7 @@ This will allow you to see the some live code running in your browser. Just pick [TubeFilter]: ../docs/gallery/TubeFilter.jpg [Cutter]: ../docs/gallery/Cutter.jpg [PolyDataNormals]: ../docs/gallery/PolyDataNormals.jpg +[ThresholdPoints]: ../docs/gallery/ThresholdPoints.jpg # Sources diff --git a/Sources/Filters/Core/ThresholdPoints/example/controlPanel.html b/Sources/Filters/Core/ThresholdPoints/example/controlPanel.html new file mode 100644 index 00000000000..774cab3b5c0 --- /dev/null +++ b/Sources/Filters/Core/ThresholdPoints/example/controlPanel.html @@ -0,0 +1,28 @@ + + + + + + + + + + + + + +
Arrays to threshold + +
Operation + +
Treshold value + +
\ No newline at end of file diff --git a/Sources/Filters/Core/ThresholdPoints/example/index.js b/Sources/Filters/Core/ThresholdPoints/example/index.js new file mode 100644 index 00000000000..5bdcdc4ecd0 --- /dev/null +++ b/Sources/Filters/Core/ThresholdPoints/example/index.js @@ -0,0 +1,176 @@ +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 '@kitware/vtk.js/IO/Core/DataAccessHelper/HttpDataAccessHelper'; + +import vtkActor from '@kitware/vtk.js/Rendering/Core/Actor'; +import vtkHttpDataSetReader from '@kitware/vtk.js/IO/Core/HttpDataSetReader'; +import vtkLookupTable from '@kitware/vtk.js/Common/Core/LookupTable'; +import vtkMapper from '@kitware/vtk.js/Rendering/Core/Mapper'; +import vtkThresholdPoints from '@kitware/vtk.js/Filters/Core/ThresholdPoints'; +import vtkCalculator from '@kitware/vtk.js/Filters/General/Calculator'; +import { FieldDataTypes } from '@kitware/vtk.js/Common/DataModel/DataSet/Constants'; +import { AttributeTypes } from '@kitware/vtk.js/Common/DataModel/DataSetAttributes/Constants'; +import vtkScalarBarActor from '@kitware/vtk.js/Rendering/Core/ScalarBarActor'; + +import controlPanel from './controlPanel.html'; + +const { ColorMode, ScalarMode } = vtkMapper; + +// ---------------------------------------------------------------------------- +// Standard rendering code setup +// ---------------------------------------------------------------------------- + +const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({ + background: [0.9, 0.9, 0.9], +}); +fullScreenRenderer.addController(controlPanel); + +const renderer = fullScreenRenderer.getRenderer(); +const renderWindow = fullScreenRenderer.getRenderWindow(); + +// ---------------------------------------------------------------------------- +// Example code +// ---------------------------------------------------------------------------- + +const lookupTable = vtkLookupTable.newInstance({ hueRange: [0.666, 0] }); + +const reader = vtkHttpDataSetReader.newInstance({ fetchGzip: true }); +reader.setUrl(`${__BASE_PATH__}/data/cow.vtp`).then(() => { + reader.loadData().then(() => { + renderer.resetCamera(); + renderWindow.render(); + }); +}); + +const calc = vtkCalculator.newInstance(); +calc.setInputConnection(reader.getOutputPort()); +calc.setFormula({ + getArrays: (inputDataSets) => ({ + input: [{ location: FieldDataTypes.COORDINATE }], // Require point coordinates as input + output: [ + // Generate two output arrays: + { + location: FieldDataTypes.POINT, // This array will be point-data ... + name: 'sine wave', // ... with the given name ... + dataType: 'Float64Array', // ... of this type ... + attribute: AttributeTypes.SCALARS, // ... and will be marked as the default scalars. + }, + { + location: FieldDataTypes.UNIFORM, // This array will be field data ... + name: 'global', // ... with the given name ... + dataType: 'Float32Array', // ... of this type ... + numberOfComponents: 1, // ... with this many components ... + tuples: 1, // ... and this many tuples. + }, + ], + }), + evaluate: (arraysIn, arraysOut) => { + // Convert in the input arrays of vtkDataArrays into variables + // referencing the underlying JavaScript typed-data arrays: + const [coords] = arraysIn.map((d) => d.getData()); + const [sine, glob] = arraysOut.map((d) => d.getData()); + + // Since we are passed coords as a 3-component array, + // loop over all the points and compute the point-data output: + for (let i = 0, sz = coords.length / 3; i < sz; ++i) { + const dx = coords[3 * i] - 0.5; + const dy = coords[3 * i + 1] - 0.5; + sine[i] = 10 * dx * dx + dy * dy; + } + // Use JavaScript's reduce method to sum the output + // point-data array and set the uniform array's value: + glob[0] = sine.reduce((result, value) => result + value, 0); + // Mark the output vtkDataArray as modified + arraysOut.forEach((x) => x.modified()); + }, +}); + +const mapper = vtkMapper.newInstance({ + interpolateScalarsBeforeMapping: true, + colorMode: ColorMode.DEFAULT, + scalarMode: ScalarMode.DEFAULT, + useLookupTableScalarRange: true, + lookupTable, +}); +const actor = vtkActor.newInstance(); +actor.getProperty().setEdgeVisibility(true); + +const scalarBarActor = vtkScalarBarActor.newInstance(); +scalarBarActor.setScalarsToColors(lookupTable); +renderer.addActor(scalarBarActor); + +const thresholder = vtkThresholdPoints.newInstance(); +thresholder.setInputConnection(calc.getOutputPort()); + +mapper.setInputConnection(thresholder.getOutputPort()); +actor.setMapper(mapper); +renderer.addActor(actor); + +// ---------------------------------------------------------------------------- +// UI control handling +// ---------------------------------------------------------------------------- + +const thresholdArray = document.querySelector('#thresholdArray'); +const thresholdOperation = document.querySelector('#thresholdOperation'); +const thresholdValue = document.querySelector('#thresholdValue'); +function updateCriterias(arrayName, operation, value) { + thresholder.setCriterias([ + { + arrayName, + fieldAssociation: arrayName === 'sine wave' ? 'PointData' : 'Points', + operation, + value: Number(value), + }, + ]); +} +function onCriteriaChanged(event) { + updateCriterias( + thresholdArray.value, + thresholdOperation.value, + thresholdValue.value + ); + if (event != null) { + renderWindow.render(); + } +} +onCriteriaChanged(); +function onArrayChanged(event) { + if (thresholdArray.value === 'x' || thresholdArray.value === 'y') { + thresholdValue.min = '-5'; + thresholdValue.max = '5'; + thresholdValue.step = '1'; + thresholdValue.value = '0'; + } else if (thresholdArray.value === 'z') { + thresholdValue.min = '-2'; + thresholdValue.max = '2'; + thresholdValue.step = '0.1'; + thresholdValue.value = '0'; + } else { + thresholdValue.min = 0; + thresholdValue.max = 256; + thresholdValue.step = 10; + thresholdValue.value = 30; + } + onCriteriaChanged(event); +} +thresholdArray.addEventListener('change', onArrayChanged); +thresholdOperation.addEventListener('change', onCriteriaChanged); +thresholdValue.addEventListener('input', onCriteriaChanged); + +// ----------------------------------------------------------- +// 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 = reader; +global.renderer = renderer; +global.renderWindow = renderWindow; +global.lookupTable = lookupTable; +global.thresholder = thresholder; diff --git a/Sources/Filters/Core/ThresholdPoints/index.d.ts b/Sources/Filters/Core/ThresholdPoints/index.d.ts new file mode 100644 index 00000000000..5eaeb484ed0 --- /dev/null +++ b/Sources/Filters/Core/ThresholdPoints/index.d.ts @@ -0,0 +1,72 @@ +import { vtkAlgorithm, vtkObject } from '../../../interfaces'; + +export interface ThresholdCriteria { + arrayName: string; + fieldAssociation: string; + operation: string; + value: number; +} + +/** + * + */ +export interface IThresholdPointsInitialValues { + criterias?: ThresholdCriteria[]; +} + +type vtkThresholdPointsBase = vtkObject & vtkAlgorithm; + +export interface vtkThresholdPoints extends vtkThresholdPointsBase { + /** + * Get the desired precision for the output types. + */ + getCriterias(): ThresholdCriteria[]; + + /** + * Set the desired precision for the output types. + * @param outputPointsPrecision + */ + setCriterias(criterias: ThresholdCriteria[]): boolean; + + /** + * + * @param inData + * @param outData + */ + requestData(inData: any, outData: any): void; +} + +/** + * Method used to decorate a given object (publicAPI+model) with vtkThresholdPoints characteristics. + * + * @param publicAPI object on which methods will be bounds (public) + * @param model object on which data structure will be bounds (protected) + * @param {IThresholdPointsInitialValues} [initialValues] (default: {}) + */ +export function extend( + publicAPI: object, + model: object, + initialValues?: IThresholdPointsInitialValues +): void; + +/** + * Method used to create a new instance of vtkThresholdPoints + * @param {IThresholdPointsInitialValues} [initialValues] for pre-setting some of its content + */ +export function newInstance( + initialValues?: IThresholdPointsInitialValues +): vtkThresholdPoints; + +/** + * vtkThresholdPoints - extracts points whose scalar value satisfies threshold criterion + * + * vtkThresholdPoints is a filter that extracts points from a dataset that + * satisfy a threshold criterion. The criterion can take three forms: + * 1) greater than a particular value; 2) less than a particular value; or + * 3) between a particular value. The output of the filter is polygonal data. + */ +export declare const vtkThresholdPoints: { + newInstance: typeof newInstance; + extend: typeof extend; +}; +export default vtkThresholdPoints; diff --git a/Sources/Filters/Core/ThresholdPoints/index.js b/Sources/Filters/Core/ThresholdPoints/index.js new file mode 100644 index 00000000000..6df86871114 --- /dev/null +++ b/Sources/Filters/Core/ThresholdPoints/index.js @@ -0,0 +1,251 @@ +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 vtkPoints from 'vtk.js/Sources/Common/Core/Points'; +import vtkPolyData from 'vtk.js/Sources/Common/DataModel/PolyData'; +import { POLYDATA_FIELDS } from 'vtk.js/Sources/Common/DataModel/PolyData/Constants'; + +const { vtkErrorMacro } = macro; + +const OperationType = { + Below: 'Below', + Above: 'Above', +}; + +// Function to perform binary search on a sorted array +function binarySearch(items, value) { + let firstIndex = 0; + let lastIndex = items.length - 1; + let middleIndex = Math.floor((lastIndex + firstIndex) / 2); + + while (items[middleIndex] !== value && firstIndex < lastIndex) { + if (value < items[middleIndex]) { + lastIndex = middleIndex - 1; + } else if (value > items[middleIndex]) { + firstIndex = middleIndex + 1; + } + middleIndex = Math.floor((lastIndex + firstIndex) / 2); + } + + return { + found: items[middleIndex] === value, + index: Math.max( + items[middleIndex] < value ? middleIndex + 1 : middleIndex, + 0 + ), + }; +} + +function camelize(str) { + return str + .replace(/(?:^\w|[A-Z]|\b\w)/g, (letter) => letter.toUpperCase()) + .replace(/\s+/g, ''); +} +// ---------------------------------------------------------------------------- +// vtkThresholdPoints methods +// ---------------------------------------------------------------------------- + +function vtkThresholdPoints(publicAPI, model) { + // Set our classname + model.classHierarchy.push('vtkThresholdPoints'); + + publicAPI.requestData = (inData, outData) => { + const input = inData[0]; + const output = vtkPolyData.newInstance(); + outData[0] = output; + + if (model.criterias.length === 0) { + output.shallowCopy(input); + return; + } + const oldPoints = input.getPoints(); + const oldPointCount = oldPoints.getNumberOfPoints(); + const oldPointData = input.getPointData(); + const oldPointsData = oldPoints.getData(); + const newPointsData = macro.newTypedArray( + input.getPoints().getDataType(), + 3 * oldPointCount + ); + const oldArrays = []; + const newArraysData = []; + const numArrays = oldPointData.getNumberOfArrays(); + for (let i = 0; i < numArrays; ++i) { + const oldArray = oldPointData.getArrayByIndex(i); + oldArrays.push(oldArray); + newArraysData.push( + macro.newTypedArray( + oldArray.getDataType(), + oldPointCount * oldArray.getNumberOfComponents() + ) + ); + } + const pointAcceptanceFunctions = model.criterias.map((criteria) => { + let inputArray = null; + let component = 0; + let numberOfComponents = 1; + if (criteria.fieldAssociation === 'PointData') { + inputArray = oldArrays.find( + (oldArray) => oldArray.getName() === criteria.arrayName + ); + numberOfComponents = inputArray.getNumberOfComponents(); + } else if (criteria.fieldAssociation === 'Points') { + inputArray = oldPoints; + if (criteria.arrayName === 'z') { + component = 2; + } else { + component = criteria.arrayName === 'y' ? 1 : 0; + } + numberOfComponents = 3; + } else { + vtkErrorMacro('No field association'); + } + const inputArrayData = inputArray.getData(); + const operation = + criteria.operation === OperationType.Below + ? (a, b) => a < b + : (a, b) => a > b; + const pointAcceptanceFunction = (pointId) => + operation( + inputArrayData[numberOfComponents * pointId + component], + criteria.value + ); + return pointAcceptanceFunction; + }); + + const thresholdedPointIds = []; // sorted list + let newI = 0; + for (let i = 0; i < oldPointCount; ++i) { + const keepPoint = pointAcceptanceFunctions.reduce( + (keep, pointAcceptanceFunction) => keep && pointAcceptanceFunction(i), + true + ); + if (keepPoint) { + let ii = 3 * i; + let newII = 3 * newI; + for (let c = 0; c < 3; ++c) { + newPointsData[newII++] = oldPointsData[ii++]; + } + for (let j = 0; j < numArrays; ++j) { + const oldArrayData = oldArrays[j].getData(); + const newArrayData = newArraysData[j]; + const cc = oldArrays[j].getNumberOfComponents(); + ii = cc * i; + newII = cc * newI; + for (let c = 0; c < cc; ++c) { + newArrayData[newII++] = oldArrayData[ii++]; + } + } + ++newI; + } else { + thresholdedPointIds.push(i); + } + } + if (thresholdedPointIds.length === 0) { + output.shallowCopy(input); + return; + } + + output.setPoints( + vtkPoints.newInstance({ values: newPointsData, size: 3 * newI }) + ); + for (let i = 0; i < numArrays; ++i) { + const oldArray = oldArrays[i]; + const newArray = vtkDataArray.newInstance({ + name: oldArray.getName(), + values: newArraysData[i], + dataType: oldArray.getDataType(), + numberOfComponents: oldArray.getNumberOfComponents(), + size: newI * oldArray.getNumberOfComponents(), + }); + output.getPointData().addArray(newArray); + oldPointData.getAttributes(oldArray).forEach((attrType) => { + output.getPointData().setAttribute(newArray, attrType); + }); + } + + POLYDATA_FIELDS.forEach((cellType) => { + const oldPolysData = input[`get${camelize(cellType)}`]().getData(); + const newCellData = macro.newTypedArray( + input.getPolys().getDataType(), + oldPolysData.length + ); + const newPointIds = []; // first point starts at [1] + const firstPointIndex = cellType === 'verts' ? 0 : 1; + let numberOfPoints = 1; + let newP = 0; + for ( + let c = 0; + c < oldPolysData.length; + c += numberOfPoints + firstPointIndex + ) { + if (firstPointIndex === 1) { + // not for verts + numberOfPoints = oldPolysData[c]; + } + let keepCell = true; + + for (let p = firstPointIndex; p <= numberOfPoints; ++p) { + const { found, index } = binarySearch( + thresholdedPointIds, + oldPolysData[c + p] + ); + if (found) { + keepCell = false; + break; + } + newPointIds[p] = oldPolysData[c + p] - index; + } + if (keepCell) { + newCellData[newP++] = numberOfPoints; + for (let p = firstPointIndex; p <= numberOfPoints; ) { + newCellData[newP++] = newPointIds[p++]; + } + } + } + output[`set${camelize(cellType)}`]( + vtkCellArray.newInstance({ + values: newCellData, + size: newP, // it may shorter than original array if cells are not kept + dataType: input.getPolys().getDataType(), + }) + ); + }); + + outData[0] = output; + }; +} + +// ---------------------------------------------------------------------------- + +function defaultValues(publicAPI, model, initialValues = {}) { + return { + criterias: [], // arrayName: string, fieldAssociation: string, operation: string, value: number + ...initialValues, + }; +} + +// ---------------------------------------------------------------------------- + +export function extend(publicAPI, model, initialValues = {}) { + Object.assign(model, defaultValues(publicAPI, model, initialValues)); + + // Build VTK API + macro.setGet(publicAPI, model, []); + macro.get(publicAPI, model, []); + macro.setGetArray(publicAPI, model, ['criterias']); + + // Make this a VTK object + macro.obj(publicAPI, model); + macro.algo(publicAPI, model, 1, 1); + + // Object specific methods + vtkThresholdPoints(publicAPI, model); +} + +// ---------------------------------------------------------------------------- + +export const newInstance = macro.newInstance(extend, 'vtkThresholdPoints'); + +// ---------------------------------------------------------------------------- + +export default { newInstance, extend, OperationType };