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 @@
+
\ 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 };