Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(filter): Add contour loop extraction filter #3003

Merged
merged 3 commits into from Feb 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -0,0 +1,74 @@
<table>
<tr>
<td><b>Origin<b></td>
</tr>
<tr>
<td>X</td>
<td>
<form name='originXForm'>
<input class='originX' id="originXInputId" type="range"
min="-6" max="6" step="0.01" value="0"
oninput="originXOutputId.value = originXInputId.value"/>
<output id="originXOutputId">0</output>
</form>
</td>
</tr>
<tr>
<td>Y</td>
<td>
<form name='originYForm'>
<input class='originY' id="originYInputId" type="range"
min="-0.5" max="0.5" step="0.01" value="0"
oninput="originYOutputId.value = originYInputId.value"/>
<output id="originYOutputId">0</output>
</form>
</td>
</tr>
<tr>
<td>Z</td>
<td>
<form name='originZForm'>
<input class='originZ' id="originZInputId" type="range"
min="-0.5" max="0.5" step="0.01" value="0"
oninput="originZOutputId.value = originZInputId.value"/>
<output id="originZOutputId">0</output>
</form>
</td>
</tr>
<tr>
<td><b>Normal<b></td>
</tr>
<tr>
<td>X</td>
<td>
<form name='normalXForm'>
<input class='normalX' id="normalXInputId" type="range"
min="-1" max="1" step="0.01" value="1"
oninput="normalXOutputId.value = normalXInputId.value"/>
<output id="normalXOutputId">1</output>
</form>
</td>
</tr>
<tr>
<td>Y</td>
<td>
<form name='normalYForm'>
<input class='normalY' id="normalYInputId" type="range"
min="-1" max="1" step="0.01" value="0"
oninput="normalYOutputId.value = normalYInputId.value"/>
<output id="normalYOutputId">0</output>
</form>
</td>
</tr>
<tr>
<td>Z</td>
<td>
<form name='normalZForm'>
<input class='normalZ' id="normalZInputId" type="range"
min="-1" max="1" step="0.01" value="0"
oninput="normalZOutputId.value = normalZInputId.value"/>
<output id="normalZOutputId">0</output>
</form>
</td>
</tr>
</table>
205 changes: 205 additions & 0 deletions 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();
});
},
});
});
79 changes: 79 additions & 0 deletions 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;