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 +
+ + 0 +
+
Y +
+ + 0 +
+
Z +
+ + 0 +
+
Normal
X +
+ + 1 +
+
Y +
+ + 0 +
+
Z +
+ + 0 +
+
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 };