diff --git a/Documentation/content/docs/gallery/FrustumSource.jpg b/Documentation/content/docs/gallery/FrustumSource.jpg
new file mode 100644
index 00000000000..4f161a482ca
Binary files /dev/null and b/Documentation/content/docs/gallery/FrustumSource.jpg differ
diff --git a/Documentation/content/examples/index.md b/Documentation/content/examples/index.md
index 8b96dbf7291..8c422db4dad 100644
--- a/Documentation/content/examples/index.md
+++ b/Documentation/content/examples/index.md
@@ -144,6 +144,7 @@ This will allow you to see the some live code running in your browser. Just pick
[![Cursor3D Example][Cursor3D]](./Cursor3D.html "Cursor3D")
[![CylinderSource Example][CylinderSource]](./CylinderSource.html "CylinderSource")
[![EllipseArcSource Example][EllipseArcSource]](./EllipseArcSource.html "EllipseArcSource")
+[![FrustumSource Example][FrustumSource]](./FrustumSource.html "FrustumSource")
[![LineSource Example][LineSource]](./LineSource.html "LineSource")
[![PlaneSource Example][PlaneSource]](./PlaneSource.html "PlaneSource")
[![PointSource Example][PointSource]](./PointSource.html "PointSource")
@@ -164,6 +165,7 @@ This will allow you to see the some live code running in your browser. Just pick
[Cursor3D]: ../docs/gallery/Cursor3D.gif
[CylinderSource]: ../docs/gallery/CylinderSource.jpg
[EllipseArcSource]: ../docs/gallery/EllipseArcSource.jpg
+[FrustumSource]: ../docs/gallery/FrustumSource.jpg
[LineSource]: ../docs/gallery/LineSource.jpg
[PlaneSource]: ../docs/gallery/PlaneSource.jpg
[PointSource]: ../docs/gallery/PointSource.jpg
diff --git a/Sources/Filters/Sources/FrustumSource/example/controlPanel.html b/Sources/Filters/Sources/FrustumSource/example/controlPanel.html
new file mode 100644
index 00000000000..3c9f6eaf825
--- /dev/null
+++ b/Sources/Filters/Sources/FrustumSource/example/controlPanel.html
@@ -0,0 +1,15 @@
+
+
diff --git a/Sources/Filters/Sources/FrustumSource/example/index.js b/Sources/Filters/Sources/FrustumSource/example/index.js
new file mode 100644
index 00000000000..f1822155ac4
--- /dev/null
+++ b/Sources/Filters/Sources/FrustumSource/example/index.js
@@ -0,0 +1,85 @@
+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 vtkFullScreenRenderWindow from '@kitware/vtk.js/Rendering/Misc/FullScreenRenderWindow';
+import vtkActor from '@kitware/vtk.js/Rendering/Core/Actor';
+import vtkMapper from '@kitware/vtk.js/Rendering/Core/Mapper';
+import vtkProperty from '@kitware/vtk.js/Rendering/Core/Property';
+import vtkCamera from '@kitware/vtk.js/Rendering/Core/Camera';
+import vtkPlanes from '@kitware/vtk.js/Common/DataModel/Planes';
+import vtkFrustumSource from '@kitware/vtk.js/Filters/Sources/FrustumSource';
+import vtkShrinkPolyData from '@kitware/vtk.js/Filters/General/ShrinkPolyData';
+import controlPanel from './controlPanel.html';
+
+// ----------------------------------------------------------------------------
+// Standard rendering code setup
+// ----------------------------------------------------------------------------
+
+const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance();
+const renderer = fullScreenRenderer.getRenderer();
+const renderWindow = fullScreenRenderer.getRenderWindow();
+
+// ----------------------------------------------------------------------------
+// Example code
+// ----------------------------------------------------------------------------
+
+const frustum = vtkFrustumSource.newInstance();
+const actor = vtkActor.newInstance();
+const mapper = vtkMapper.newInstance();
+
+const camera = vtkCamera.newInstance();
+camera.setClippingRange(0.1, 0.4);
+
+const planesArray = camera.getFrustumPlanes(1.0);
+const planes = vtkPlanes.newInstance();
+planes.setFrustumPlanes(planesArray);
+
+frustum.setPlanes(planes);
+frustum.setShowLines(false);
+
+const backProperty = vtkProperty.newInstance();
+backProperty.setColor(1, 1, 50 / 255);
+
+const shrink = vtkShrinkPolyData.newInstance();
+shrink.setShrinkFactor(0.9);
+shrink.setInputConnection(frustum.getOutputPort());
+mapper.setInputConnection(shrink.getOutputPort());
+mapper.setScalarVisibility(false);
+actor.setMapper(mapper);
+actor.getProperty().setColor(255 / 255, 99 / 255, 71 / 255);
+actor.getProperty().setEdgeVisibility(true);
+actor.setBackfaceProperty(backProperty);
+renderer.addActor(actor);
+
+renderer.getActiveCamera().setPosition(1, 0, 0);
+renderer.getActiveCamera().setFocalPoint(0, 0, 0);
+renderer.getActiveCamera().setViewUp(0, 1, 0);
+renderer.getActiveCamera().azimuth(30);
+renderer.getActiveCamera().elevation(30);
+
+renderer.resetCamera();
+renderWindow.render();
+
+// -----------------------------------------------------------
+// UI control handling
+// -----------------------------------------------------------
+
+fullScreenRenderer.addController(controlPanel);
+
+['showLines'].forEach((propertyName) => {
+ document.querySelector(`.${propertyName}`).addEventListener('input', (e) => {
+ const value = e.target.checked;
+ frustum.set({ [propertyName]: value });
+ renderWindow.render();
+ });
+});
+
+['shrinkFactor'].forEach((propertyName) => {
+ document.querySelector(`.${propertyName}`).addEventListener('input', (e) => {
+ const value = Number(e.target.value);
+ shrink.set({ [propertyName]: value });
+ renderWindow.render();
+ });
+});
diff --git a/Sources/Filters/Sources/FrustumSource/index.d.ts b/Sources/Filters/Sources/FrustumSource/index.d.ts
new file mode 100644
index 00000000000..06e1e3066f6
--- /dev/null
+++ b/Sources/Filters/Sources/FrustumSource/index.d.ts
@@ -0,0 +1,111 @@
+import { DesiredOutputPrecision } from '../../../Common/DataModel/DataSetAttributes';
+import { vtkAlgorithm, vtkObject } from '../../../interfaces';
+import vtkPlanes from '../../../Common/DataModel/Planes';
+
+/**
+ *
+ */
+export interface IFrustumSourceInitialValues {
+ planes?: vtkPlanes;
+ showLines?: boolean;
+ outputPointsPrecision?: DesiredOutputPrecision;
+}
+
+type vtkFrustumSourceBase = vtkObject &
+ Omit<
+ vtkAlgorithm,
+ | 'getInputData'
+ | 'setInputData'
+ | 'setInputConnection'
+ | 'getInputConnection'
+ | 'addInputConnection'
+ | 'addInputData'
+ >;
+
+export interface vtkFrustumSource extends vtkFrustumSourceBase {
+ /**
+ * Get the output points precision.
+ */
+ getOutputPointsPrecision(): DesiredOutputPrecision;
+
+ /**
+ * Get the planes defining the frustum.
+ */
+ getPlanes(): vtkPlanes;
+
+ /**
+ * Get whether to show lines.
+ */
+ getShowLines(): boolean;
+
+ /**
+ *
+ * @param inData
+ * @param outData
+ */
+ requestData(inData: any, outData: any): void;
+
+ /**
+ * Set the output points precision.
+ * @param {DesiredOutputPrecision} precision
+ */
+ setOutputPointsPrecision(precision: DesiredOutputPrecision): boolean;
+
+ /**
+ * Set the planes defining the frustum.
+ * @param {vtkPlanes} planes
+ */
+ setPlanes(planes: vtkPlanes): boolean;
+
+ /**
+ * Set whether to show lines.
+ * @param {Boolean} showLines
+ */
+ setShowLines(showLines: boolean): boolean;
+}
+
+/**
+ * Method used to decorate a given object (publicAPI+model) with vtkFrustumSource characteristics.
+ *
+ * @param publicAPI object on which methods will be bounds (public)
+ * @param model object on which data structure will be bounds (protected)
+ * @param {IFrustumSourceInitialValues} [initialValues] (default: {})
+ */
+export function extend(
+ publicAPI: object,
+ model: object,
+ initialValues?: IFrustumSourceInitialValues
+): void;
+
+/**
+ * Method used to create a new instance of vtkFrustumSource.
+ * @param {IFrustumSourceInitialValues} [initialValues] for pre-setting some of its content
+ */
+export function newInstance(
+ initialValues?: IFrustumSourceInitialValues
+): vtkFrustumSource;
+
+/**
+ * vtkFrustumSource creates a frustum defines by a set of planes. The frustum is
+ * represented with four-sided polygons. It is possible to specify extra lines
+ * to better visualize the field of view.
+ *
+ * @example
+ * ```js
+ * import vtkFrustumSource from '@kitware/vtk.js/Filters/Sources/FrustumSource';
+ *
+ * const frustum = vtkFrustumSource.newInstance();
+ * const camera = vtkCamera.newInstance();
+ * camera.setClippingRange(0.1, 0.4);
+ * const planesArray = camera.getFrustumPlanes(1.0);
+ * const planes = vtkPlanes.newInstance();
+ * planes.setFrustumPlanes(planesArray);
+ * frustum.setPlanes(planes);
+ * frustum.setShowLines(false);
+ * ```
+ */
+export declare const vtkFrustumSource: {
+ newInstance: typeof newInstance;
+ extend: typeof extend;
+};
+export default vtkFrustumSource;
diff --git a/Sources/Filters/Sources/FrustumSource/index.js b/Sources/Filters/Sources/FrustumSource/index.js
new file mode 100644
index 00000000000..51d894a78b8
--- /dev/null
+++ b/Sources/Filters/Sources/FrustumSource/index.js
@@ -0,0 +1,277 @@
+import macro from 'vtk.js/Sources/macros';
+import vtkMath from 'vtk.js/Sources/Common/Core/Math';
+import vtkCellArray from 'vtk.js/Sources/Common/Core/CellArray';
+import vtkPoints from 'vtk.js/Sources/Common/Core/Points';
+import vtkPolyData from 'vtk.js/Sources/Common/DataModel/PolyData';
+import { DesiredOutputPrecision } from 'vtk.js/Sources/Common/DataModel/DataSetAttributes/Constants';
+import { VtkDataTypes } from 'vtk.js/Sources/Common/Core/DataArray/Constants';
+
+const { vtkErrorMacro } = macro;
+
+// ----------------------------------------------------------------------------
+// vtkFrustumSource methods
+// ----------------------------------------------------------------------------
+
+function vtkFrustumSource(publicAPI, model) {
+ // Set our className
+ model.classHierarchy.push('vtkFrustumSource');
+
+ function computePoint(planeIndices, pt) {
+ // Get planes and their normals/origins
+ const plane0 = model.planes.getPlane(planeIndices[0]);
+ const n0 = plane0.getNormal();
+ const p0 = plane0.getOrigin();
+
+ const plane1 = model.planes.getPlane(planeIndices[1]);
+ const n1 = plane1.getNormal();
+ const p1 = plane1.getOrigin();
+
+ const plane2 = model.planes.getPlane(planeIndices[2]);
+ const n2 = plane2.getNormal();
+ const p2 = plane2.getOrigin();
+
+ // Dot products
+ const d0 = vtkMath.dot(p0, n0);
+ const d1 = vtkMath.dot(p1, n1);
+ const d2 = vtkMath.dot(p2, n2);
+
+ // Cross products
+ const c12 = [0, 0, 0];
+ vtkMath.cross(n1, n2, c12);
+ const c20 = [0, 0, 0];
+ vtkMath.cross(n2, n0, c20);
+ const c01 = [0, 0, 0];
+ vtkMath.cross(n0, n1, c01);
+
+ // Determinant
+ const d = vtkMath.determinant3x3([...n0, ...n1, ...n2]);
+
+ // Intersection point
+ for (let i = 0; i < 3; ++i) {
+ pt[i] = (d0 * c12[i] + d1 * c20[i] + d2 * c01[i]) / d;
+ }
+ }
+
+ publicAPI.requestData = (inData, outData) => {
+ const output = outData[0] || vtkPolyData.newInstance();
+
+ if (!model.planes || model.planes.getNumberOfPlanes() !== 6) {
+ vtkErrorMacro('vtkFrustum requires 6 planes.');
+ return;
+ }
+
+ let nbPts = 8;
+
+ let leftRightNull = false;
+ let bottomTopNull = false;
+ let parallelFrustum = false;
+
+ // angle between left and right planes
+ const n0 = model.planes.getPlane(0).getNormal().slice();
+ const n1 = model.planes.getPlane(1).getNormal().slice();
+ const c = [0, 0, 0];
+
+ vtkMath.normalize(n0);
+ vtkMath.normalize(n1);
+ vtkMath.dot(n0, n1);
+
+ vtkMath.cross(n0, n1, c);
+ vtkMath.norm(c);
+
+ // angle between bottom and top planes
+ n0.splice(0, 3, ...model.planes.getPlane(2).getNormal());
+ n1.splice(0, 3, ...model.planes.getPlane(3).getNormal());
+
+ vtkMath.normalize(n0);
+ vtkMath.normalize(n1);
+ vtkMath.dot(n0, n1);
+
+ vtkMath.cross(n0, n1, c);
+ vtkMath.norm(c);
+
+ if (model.showLines) {
+ const left = model.planes.getPlane(0).getNormal();
+ const right = model.planes.getPlane(1).getNormal();
+ const bottom = model.planes.getPlane(2).getNormal();
+ const top = model.planes.getPlane(3).getNormal();
+
+ const leftRight = [0, 0, 0];
+ vtkMath.cross(left, right, leftRight);
+
+ leftRightNull =
+ leftRight[0] === 0.0 && leftRight[1] === 0.0 && leftRight[2] === 0.0;
+
+ const bottomTop = [0, 0, 0];
+ vtkMath.cross(bottom, top, bottomTop);
+ bottomTopNull =
+ bottomTop[0] === 0.0 && bottomTop[1] === 0.0 && bottomTop[2] === 0.0;
+ parallelFrustum = leftRightNull && bottomTopNull;
+
+ if (parallelFrustum) {
+ // start at near points, just add the 4 extra far points.
+ nbPts += 4;
+ } else if (leftRightNull || bottomTopNull) {
+ // two extra starting points, and 4 extra far points.
+ nbPts += 6;
+ } else {
+ // there is an apex, and 4 extra far points
+ nbPts += 5;
+ }
+ }
+
+ let pointType = 0;
+ if (model.outputPointsPrecision === DesiredOutputPrecision.SINGLE) {
+ pointType = VtkDataTypes.FLOAT;
+ } else if (model.outputPointsPrecision === DesiredOutputPrecision.DOUBLE) {
+ pointType = VtkDataTypes.DOUBLE;
+ }
+ const newPoints = vtkPoints.newInstance({ dataType: pointType });
+
+ newPoints.setNumberOfPoints(nbPts);
+
+ const pt = [0.0, 0.0, 0.0];
+ const planes = [0, 0, 0];
+
+ planes[0] = 0; // left
+ planes[1] = 2; // bottom
+ planes[2] = 5; // near
+ computePoint(planes, pt);
+ newPoints.setPoint(0, ...pt);
+
+ planes[0] = 1;
+ computePoint(planes, pt);
+ newPoints.setPoint(1, ...pt);
+
+ planes[1] = 3;
+ computePoint(planes, pt);
+ newPoints.setPoint(2, ...pt);
+
+ planes[0] = 0;
+ computePoint(planes, pt);
+ newPoints.setPoint(3, ...pt);
+
+ planes[1] = 2;
+ planes[2] = 4; // far
+ computePoint(planes, pt);
+ newPoints.setPoint(4, ...pt);
+
+ planes[0] = 1;
+ computePoint(planes, pt);
+ newPoints.setPoint(5, ...pt);
+
+ planes[1] = 3;
+ computePoint(planes, pt);
+ newPoints.setPoint(6, ...pt);
+
+ planes[0] = 0;
+ computePoint(planes, pt);
+ newPoints.setPoint(7, ...pt);
+
+ const numPolys = 6;
+ const newPolys = vtkCellArray.newInstance();
+ newPolys.allocate(numPolys * 5);
+
+ // left
+ newPolys.insertNextCell([4, 0, 3, 7]);
+
+ // right
+ newPolys.insertNextCell([1, 5, 6, 2]);
+
+ // bottom
+ newPolys.insertNextCell([0, 4, 5, 1]);
+
+ // top
+ newPolys.insertNextCell([3, 2, 6, 7]);
+
+ // near
+ newPolys.insertNextCell([0, 1, 2, 3]);
+
+ // far
+ newPolys.insertNextCell([4, 7, 6, 5]);
+
+ let newLines = null;
+ if (model.showLines) {
+ const numLines = 4;
+ newLines = vtkCellArray.newInstance();
+ newLines.allocate(numLines * 3);
+
+ const pts = [12, 8]; // apex, or first of the two extra near points.
+
+ // line from lower-left corner
+ if (parallelFrustum) {
+ pts[0] = 0;
+ }
+ newLines.insertNextCell(pts);
+
+ // line from lower-right corner
+ if (parallelFrustum) {
+ pts[0]++;
+ } else if (leftRightNull) {
+ pts[0] = 13;
+ }
+ pts[1]++;
+ newLines.insertNextCell(pts);
+
+ // line from upper-right corner
+ if (parallelFrustum) {
+ pts[0]++;
+ } else if (bottomTopNull) {
+ pts[0] = 13;
+ }
+ pts[1]++;
+ newLines.insertNextCell(pts);
+
+ // line from upper-left corner
+ if (parallelFrustum) {
+ pts[0]++;
+ } else if (leftRightNull) {
+ pts[0] = 12;
+ }
+ pts[1]++;
+ newLines.insertNextCell(pts);
+ output.setLines(newLines);
+ }
+
+ output.setPoints(newPoints);
+ output.setPolys(newPolys);
+
+ outData[0] = output;
+ };
+}
+
+// ----------------------------------------------------------------------------
+// Object factory
+// ----------------------------------------------------------------------------
+
+const DEFAULT_VALUES = {
+ planes: null,
+ showLines: true,
+ outputPointsPrecision: DesiredOutputPrecision.DEFAULT,
+};
+
+// ----------------------------------------------------------------------------
+
+export function extend(publicAPI, model, initialValues = {}) {
+ Object.assign(model, DEFAULT_VALUES, initialValues);
+
+ // Build VTK API
+ macro.obj(publicAPI, model);
+
+ // Also make it an algorithm with no input and one output
+ macro.algo(publicAPI, model, 0, 1);
+ macro.setGet(publicAPI, model, [
+ 'showLines',
+ 'outputPointsPrecision',
+ 'planes',
+ ]);
+
+ vtkFrustumSource(publicAPI, model);
+}
+
+// ----------------------------------------------------------------------------
+
+export const newInstance = macro.newInstance(extend, 'vtkFrustumSource');
+
+// ----------------------------------------------------------------------------
+
+export default { newInstance, extend };