diff --git a/Sources/Filters/General/ContourLoopExtraction/example/controlPanel.html b/Sources/Filters/General/ContourLoopExtraction/example/controlPanel.html
new file mode 100644
index 00000000000..a8ce8fdcb49
--- /dev/null
+++ b/Sources/Filters/General/ContourLoopExtraction/example/controlPanel.html
@@ -0,0 +1,74 @@
+
+
+ Origin |
+
+
+ X |
+
+
+ |
+
+
+ Y |
+
+
+ |
+
+
+ Z |
+
+
+ |
+
+
+ Normal |
+
+
+ X |
+
+
+ |
+
+
+ Y |
+
+
+ |
+
+
+ Z |
+
+
+ |
+
+
diff --git a/Sources/Filters/General/ContourLoopExtraction/example/index.js b/Sources/Filters/General/ContourLoopExtraction/example/index.js
new file mode 100644
index 00000000000..77206be2a3a
--- /dev/null
+++ b/Sources/Filters/General/ContourLoopExtraction/example/index.js
@@ -0,0 +1,205 @@
+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 vtkActor from '@kitware/vtk.js/Rendering/Core/Actor';
+import vtkCutter from '@kitware/vtk.js/Filters/Core/Cutter';
+import vtkFullScreenRenderWindow from '@kitware/vtk.js/Rendering/Misc/FullScreenRenderWindow';
+import HttpDataAccessHelper from '@kitware/vtk.js/IO/Core/DataAccessHelper/HttpDataAccessHelper';
+import DataAccessHelper from '@kitware/vtk.js/IO/Core/DataAccessHelper';
+import vtkHttpSceneLoader from '@kitware/vtk.js/IO/Core/HttpSceneLoader';
+import vtkMapper from '@kitware/vtk.js/Rendering/Core/Mapper';
+import vtkPlane from '@kitware/vtk.js/Common/DataModel/Plane';
+import vtkProperty from '@kitware/vtk.js/Rendering/Core/Property';
+import vtkContourLoopExtraction from '@kitware/vtk.js/Filters/General/ContourLoopExtraction';
+import vtkPolyData from 'vtk.js/Sources/Common/DataModel/PolyData';
+import vtkPoints from '@kitware/vtk.js/Common/Core/Points';
+import vtkCellArray from '@kitware/vtk.js/Common/Core/CellArray';
+import controlPanel from './controlPanel.html';
+
+// Force DataAccessHelper to have access to various data source
+import '@kitware/vtk.js/IO/Core/DataAccessHelper/JSZipDataAccessHelper';
+
+// ----------------------------------------------------------------------------
+// Standard rendering code setup
+// ----------------------------------------------------------------------------
+const colors = [
+ [1, 0, 0], // Red
+ [0, 1, 0], // Green
+ [0, 0, 1], // Blue
+ [1, 1, 0], // Yellow
+ [1, 0, 1], // Magenta
+ [0, 1, 1], // Cyan
+];
+
+const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({
+ background: [0, 0, 0],
+});
+const renderer = fullScreenRenderer.getRenderer();
+const renderWindow = fullScreenRenderer.getRenderWindow();
+
+// ----------------------------------------------------------------------------
+// Example code
+// ----------------------------------------------------------------------------
+
+const plane = vtkPlane.newInstance();
+
+const cutter = vtkCutter.newInstance();
+cutter.setCutFunction(plane);
+
+const dragonMapper = vtkMapper.newInstance();
+dragonMapper.setScalarVisibility(false);
+const dragonActor = vtkActor.newInstance();
+dragonActor.setMapper(dragonMapper);
+const dragonProperty = dragonActor.getProperty();
+dragonProperty.setRepresentation(vtkProperty.Representation.WIREFRAME);
+dragonProperty.setLighting(false);
+dragonProperty.setOpacity(0.1);
+renderer.addActor(dragonActor);
+
+// -----------------------------------------------------------
+// UI control handling
+// -----------------------------------------------------------
+
+fullScreenRenderer.addController(controlPanel);
+
+const state = {
+ originX: 0,
+ originY: 0,
+ originZ: 0,
+ normalX: 1,
+ normalY: 0,
+ normalZ: 0,
+};
+
+/**
+ * Updates the plane's position and orientation based on the global state,
+ * removes all actors except the first one (presumed to be the dragon actor),
+ * and generates loops from the cutting operation to be displayed in the renderer.
+ */
+const updatePlaneAndGenerateLoops = () => {
+ // Update plane based on the current state
+ plane.setOrigin(state.originX, state.originY, state.originZ);
+ plane.setNormal(state.normalX, state.normalY, state.normalZ);
+
+ // Perform rendering
+ renderWindow.render();
+
+ // Process cutter output to extract contour loops
+ const cutterOutput = cutter.getOutputData();
+ cutterOutput.buildLinks();
+ const loopExtractor = vtkContourLoopExtraction.newInstance();
+ loopExtractor.setInputData(cutterOutput);
+
+ const outputData = loopExtractor.getOutputData();
+ const loops = outputData.getLines().getData();
+ const points = outputData.getPoints().getData();
+ const numberOfLoops = outputData.getLines().getNumberOfCells();
+
+ // Data structures to hold the extracted loops' points
+ const flatPointsAll = [];
+ const pointListsAll = [];
+ let index = 0;
+
+ // Preserve the first actor (dragon) and remove any additional actors
+ const actors = renderer.getActors();
+ for (let i = 1; i < actors.length; i++) {
+ renderer.removeActor(actors[i]);
+ }
+
+ // Extract points from each loop
+ for (let i = 0; i < numberOfLoops; i++) {
+ const polygonPointCount = loops[index];
+ const polygonPointIndices = loops.slice(
+ index + 1,
+ index + 1 + polygonPointCount
+ );
+
+ const polygon = [];
+ const pointList = [];
+ polygonPointIndices.forEach((pointIndex) => {
+ const point = [
+ points[pointIndex * 3],
+ points[pointIndex * 3 + 1],
+ points[pointIndex * 3 + 2],
+ ];
+ polygon.push(...point);
+ pointList.push(point);
+ });
+
+ flatPointsAll.push(polygon);
+ pointListsAll.push(pointList);
+ index += polygonPointCount + 1;
+ }
+
+ // Create and display loops as actors
+ pointListsAll.forEach((pointList, loopIndex) => {
+ const pointsData = vtkPoints.newInstance();
+ const linesData = vtkCellArray.newInstance();
+ const flatPoints = flatPointsAll[loopIndex];
+
+ // Create a list of point indices to define the lines
+ const pointIndexes = Float32Array.from(pointList.map((_, ind) => ind));
+ const linePoints = Float32Array.from(flatPoints);
+
+ pointsData.setData(linePoints, 3);
+ linesData.insertNextCell(Array.from(pointIndexes));
+
+ // Construct polygon from points and lines
+ const polygon = vtkPolyData.newInstance();
+ polygon.setPoints(pointsData);
+ polygon.setLines(linesData);
+
+ // Create actor for the loop
+ const actor = vtkActor.newInstance();
+ const color = colors[loopIndex % colors.length];
+ actor.getProperty().setColor(...color);
+ actor.getProperty().setLineWidth(5); // Set line thickness
+
+ const mapper = vtkMapper.newInstance();
+ mapper.setInputData(polygon);
+ actor.setMapper(mapper);
+ renderer.addActor(actor);
+ });
+
+ // Render the updated scene
+ renderWindow.render();
+};
+
+// Update when changing UI
+['originX', 'originY', 'originZ', 'normalX', 'normalY', 'normalZ'].forEach(
+ (propertyName) => {
+ const elem = document.querySelector(`.${propertyName}`);
+ elem.addEventListener('input', (e) => {
+ const value = Number(e.target.value);
+ state[propertyName] = value;
+ updatePlaneAndGenerateLoops();
+ });
+ }
+);
+
+HttpDataAccessHelper.fetchBinary(
+ `${__BASE_PATH__}/data/StanfordDragon.vtkjs`,
+ {}
+).then((zipContent) => {
+ const dataAccessHelper = DataAccessHelper.get('zip', {
+ zipContent,
+ callback: (zip) => {
+ const sceneImporter = vtkHttpSceneLoader.newInstance({
+ renderer,
+ dataAccessHelper,
+ });
+ sceneImporter.setUrl('index.json');
+ sceneImporter.onReady(() => {
+ sceneImporter.getScene()[0].actor.setVisibility(false);
+
+ const source = sceneImporter.getScene()[0].source;
+ cutter.setInputConnection(source.getOutputPort());
+ dragonMapper.setInputConnection(source.getOutputPort());
+ renderer.resetCamera();
+ updatePlaneAndGenerateLoops();
+ });
+ },
+ });
+});
diff --git a/Sources/Filters/General/ContourLoopExtraction/index.d.ts b/Sources/Filters/General/ContourLoopExtraction/index.d.ts
new file mode 100644
index 00000000000..150602e619d
--- /dev/null
+++ b/Sources/Filters/General/ContourLoopExtraction/index.d.ts
@@ -0,0 +1,79 @@
+import { vtkAlgorithm, vtkObject } from '../../../interfaces';
+import vtkPolyData from '../../../Common/DataModel/PolyData';
+/**
+ * Initial configuration values for vtkContourLoopExtraction instances.
+ */
+export interface IContourLoopExtractionInitialValues {}
+
+type vtkContourLoopExtractionBase = vtkObject & vtkAlgorithm;
+
+export interface vtkContourLoopExtraction extends vtkContourLoopExtractionBase {
+ /**
+ * Runs the contour extraction algorithm with the given input and output data.
+ * @param inData - The input data for the contour extraction.
+ * @param outData - The output data where the extracted contours will be stored.
+ */
+ requestData(inData: vtkPolyData[], outData: vtkPolyData[]): void;
+
+ /**
+ * Extracts contour loops from the given polydata input and populates the given output.
+ * @param input - The input polydata
+ * @param output - The output polydata
+ */
+ extractContours(input: vtkPolyData, output: vtkPolyData): void;
+
+ /**
+ * Traverses a loop starting from a given line and point, in a specified direction.
+ * @param pd - The polydata which to traverse.
+ * @param dir - The direction of traversal.
+ * @param startLineId - The ID of the starting line.
+ * @param startPtId - The ID of the starting point.
+ * @param loopPoints - The array to store the traversed points of the loop.
+ * @returns The last point ID after traversal.
+ */
+ traverseLoop(
+ pd: vtkPolyData,
+ dir: number,
+ startLineId: number,
+ startPtId: number,
+ loopPoints: Array<{ t: number; ptId: number }>
+ ): number;
+}
+
+// ----------------------------------------------------------------------------
+// Static API
+// ----------------------------------------------------------------------------
+
+/**
+ * Method use to decorate a given object (publicAPI+model) with vtkContourLoopExtraction characteristics.
+ *
+ * @param publicAPI - Object on which methods will be bound (public).
+ * @param model - Object on which data structure will be bound (protected).
+ * @param initialValues - (Optional) Initial values to assign to the model.
+ */
+export function extend(
+ publicAPI: object,
+ model: object,
+ initialValues?: IContourLoopExtractionInitialValues
+): void;
+
+/**
+ * Method used to create a new instance of vtkContourLoopExtraction.
+ *
+ * @param initialValues - (Optional) Initial values for the instance.
+ */
+export function newInstance(
+ initialValues?: IContourLoopExtractionInitialValues
+): vtkContourLoopExtraction;
+
+// ----------------------------------------------------------------------------
+
+/**
+ * vtkContourLoopExtraction specific static methods.
+ */
+export declare const vtkContourLoopExtraction: {
+ newInstance: typeof newInstance;
+ extend: typeof extend;
+};
+
+export default vtkContourLoopExtraction;
diff --git a/Sources/Filters/General/ContourLoopExtraction/index.js b/Sources/Filters/General/ContourLoopExtraction/index.js
new file mode 100644
index 00000000000..3e83859592c
--- /dev/null
+++ b/Sources/Filters/General/ContourLoopExtraction/index.js
@@ -0,0 +1,153 @@
+import macro from 'vtk.js/Sources/macros';
+import vtkPolyData from 'vtk.js/Sources/Common/DataModel/PolyData';
+
+const Dir = {
+ Forward: 1,
+ Backward: -1,
+};
+
+const visited = new Set();
+
+function vtkContourLoopExtraction(publicAPI, model) {
+ publicAPI.requestData = (inData, outData) => {
+ const [input] = inData;
+
+ if (!outData[0]) {
+ outData[0] = vtkPolyData.newInstance();
+ }
+ const [output] = outData;
+ publicAPI.extractContours(input, output);
+ output.modified();
+ };
+
+ publicAPI.traverseLoop = (pd, dir, startLineId, startPtId, loopPoints) => {
+ let lineId = startLineId;
+ let lastPtId = startPtId;
+ let terminated = false;
+ let numInserted = 0;
+
+ while (!terminated) {
+ const { cellPointIds } = pd.getCellPoints(lineId);
+ if (!cellPointIds) {
+ // eslint-disable-next-line no-continue
+ continue;
+ }
+
+ lastPtId =
+ cellPointIds[0] !== lastPtId ? cellPointIds[0] : cellPointIds[1];
+ numInserted++;
+
+ // parametric point value
+ const t = dir * numInserted;
+ loopPoints.push({ t, ptId: lastPtId });
+
+ const lineCell = pd.getPointCells(lastPtId);
+
+ if (lineCell.length !== 2 || lastPtId === startPtId) {
+ // looped
+ return lastPtId;
+ }
+
+ if (lineCell.length === 2) {
+ // continue along loop
+ lineId = lineCell[0] !== lineId ? lineCell[0] : lineCell[1];
+ visited.add(lineId);
+ } else {
+ // empty or invalid cell
+ terminated = true;
+ }
+ }
+
+ return lastPtId;
+ };
+
+ publicAPI.extractContours = (input, output) => {
+ const loops = [];
+ visited.clear();
+
+ const inLines = input.getLines();
+ output.getPoints().setData(Float32Array.from(input.getPoints().getData()));
+
+ // TODO skip if cached input mtime hasn't changed.
+ // iterate over input lines
+ for (let li = 0; li < inLines.getNumberOfCells(); li++) {
+ if (visited.has(li)) {
+ // eslint-disable-next-line no-continue
+ continue;
+ }
+
+ const { cellPointIds } = input.getCellPoints(li);
+ if (!cellPointIds) {
+ // eslint-disable-next-line no-continue
+ continue;
+ }
+
+ visited.add(li);
+ const startPtId = cellPointIds[0];
+
+ const loopPoints = [];
+ loopPoints.push({ t: 0, ptId: startPtId });
+
+ const endPtId = publicAPI.traverseLoop(
+ input,
+ Dir.Forward,
+ li,
+ startPtId,
+ loopPoints
+ );
+
+ if (startPtId !== endPtId) {
+ // didn't find a loop. Go other direction to see where we end up
+ publicAPI.traverseLoop(input, Dir.Backward, li, startPtId, loopPoints);
+ loopPoints.sort((a, b) => (a.t < b.t ? -1 : 1));
+ // make closed contour
+ if (
+ loopPoints.length &&
+ loopPoints[0].ptId !== loopPoints[loopPoints.length - 1]?.ptId
+ ) {
+ loopPoints.push({ ...loopPoints[loopPoints.length - 1] });
+ }
+ }
+
+ if (loopPoints.length) {
+ loops.push(loopPoints);
+ }
+ }
+
+ // clear output lines
+ const outLines = output.getLines();
+ outLines.resize(0);
+
+ loops.forEach((loop) => {
+ outLines.insertNextCell(loop.map((pt) => pt.ptId));
+ });
+ };
+}
+
+// ----------------------------------------------------------------------------
+// Object factory
+// ----------------------------------------------------------------------------
+
+const DEFAULT_VALUES = {};
+
+// ----------------------------------------------------------------------------
+
+export function extend(publicAPI, model, initialValues = {}) {
+ Object.assign(model, DEFAULT_VALUES, initialValues);
+
+ macro.obj(publicAPI, model);
+ macro.algo(publicAPI, model, 1, 1);
+
+ vtkContourLoopExtraction(publicAPI, model);
+}
+
+// ----------------------------------------------------------------------------
+
+export const newInstance = macro.newInstance(
+ extend,
+ 'vtkContourLoopExtraction'
+);
+
+// ----------------------------------------------------------------------------
+
+export default { newInstance, extend };