import macro from 'vtk.js/Sources/macros'; import * as vtkMath from 'vtk.js/Sources/Common/Core/Math'; import vtkCellArray from 'vtk.js/Sources/Common/Core/CellArray'; import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray'; import { VtkDataTypes } from 'vtk.js/Sources/Common/Core/DataArray/Constants'; import vtkPoints from 'vtk.js/Sources/Common/Core/Points'; import vtkDataSetAttributes from 'vtk.js/Sources/Common/DataModel/DataSetAttributes'; import vtkPolyData from 'vtk.js/Sources/Common/DataModel/PolyData'; import vtkContourTriangulator from 'vtk.js/Sources/Filters/General/ContourTriangulator'; import vtkEdgeLocator from 'vtk.js/Sources/Common/DataModel/EdgeLocator';
import Constants from './Constants';
const { vtkErrorMacro, capitalize } = macro; const { ScalarMode } = Constants;
function vtkClipClosedSurface(publicAPI, model) { model.classHierarchy.push('vtkClipClosedSurface');
publicAPI.getMTime = () => model.clippingPlanes.reduce( (a, b) => (b.getMTime() > a ? b.getMTime() : a), model.mtime );
function createColorValues(color1, color2, color3, colors) { const dcolors = [color1, color2, color3]; const clamp = (n, min, max) => Math.min(Math.max(n, min), max);
for (let i = 0; i < 3; i++) { for (let j = 0; j < 3; j++) { colors[i][j] = Math.round(clamp(dcolors[i][j], 0, 1) * 255); } } }
function interpolateEdge(points, pointData, locator, tol, i0, i1, v0, v1) { if (v1 > 0) { [i0, i1] = [i1, i0]; [v0, v1] = [v1, v0]; }
const edge = locator.insertUniqueEdge(i0, i1); if (edge.value != null) { return edge.value; }
const p0 = points.getPoint(i0); const p1 = points.getPoint(i1);
const f = v0 / (v0 - v1); const s = 1.0 - f; const t = 1.0 - s;
const p = [ s * p0[0] + t * p1[0], s * p0[1] + t * p1[1], s * p0[2] + t * p1[2], ];
const tol2 = tol * tol;
if (vtkMath.distance2BetweenPoints(p, p0) < tol2) { edge.value = i0; return i0; }
if (vtkMath.distance2BetweenPoints(p, p1) < tol2) { edge.value = i1; return i1; }
edge.value = points.insertNextTuple(p); pointData.interpolateData(pointData, i0, i1, edge.value, t);
return edge.value; }
function clipLines( points, pointScalars, pointData, edgeLocator, inputLines, outputLines, inLineData, outLineData ) { let numPts; let i0; let i1; let v0; let v1; let c0; let c1; const linePts = [];
const values = inputLines.getData(); let cellId = 0; for (let i = 0; i < values.length; i += numPts + 1, cellId++) { numPts = values[i]; i1 = values[i + 1]; v1 = pointScalars.getData()[i1]; c1 = v1 > 0;
for (let j = 2; j <= numPts; j++) { i0 = i1; v0 = v1; c0 = c1;
i1 = values[i + j]; v1 = pointScalars.getData()[i1]; c1 = v1 > 0;
if (c0 || c1) { if (c0 ? !c1 : c1) { linePts[c0 ? 1 : 0] = interpolateEdge( points, pointData, edgeLocator, model.tolerance, i0, i1, v0, v1 ); }
if (i0 !== i1) { linePts[0] = i0; linePts[1] = i1; const newCellId = outputLines.insertNextCell(linePts); outLineData.passData(inLineData, cellId, newCellId); } } } } }
function breakPolylines( inputLines, outputLines, inputScalars, firstLineScalar, scalars, color ) { const cellColor = [...color];
let cellId = 0; const values = inputLines.getData(); let numPts; for (let i = 0; i < values.length; i += numPts + 1) { numPts = values[i];
if (inputScalars) { inputScalars.getTuple(firstLineScalar + cellId++, cellColor); }
for (let j = 1; j < numPts; j++) { outputLines.insertNextCell([values[i + j], values[i + j + 1]]); if (scalars) { scalars.insertNextTuple(cellColor); } } } }
function copyPolygons( inputPolys, outputPolys, inputScalars, firstPolyScalar, polyScalars, color ) { if (!inputPolys) { return; }
outputPolys.deepCopy(inputPolys);
if (polyScalars) { const scalarValue = [...color]; const n = outputPolys.getNumberOfCells(); polyScalars.insertTuple(n - 1, scalarValue);
if (inputScalars) { for (let i = 0; i < n; i++) { inputScalars.getTuple(i + firstPolyScalar, scalarValue); polyScalars.setTuple(i, scalarValue); } } else { for (let i = 0; i < n; i++) { polyScalars.setTuple(i, scalarValue); } } } }
function breakTriangleStrips( inputStrips, polys, inputScalars, firstStripScalar, polyScalars, color ) { if (inputStrips.getNumberOfCells() === 0) { return; }
const values = inputStrips.getData(); let cellId = firstStripScalar; let numPts; for (let i = 0; i < values.length; i += numPts + 1, cellId++) { numPts = values[i]; let p1 = values[i + 1]; let p2 = values[i + 2];
for (let j = 0; j < numPts - 2; j++) { const p3 = values[i + j + 3]; if (j % 2) { polys.insertNextCell([p2, p1, p3]); } else { polys.insertNextCell([p1, p2, p3]); } p1 = p2; p2 = p3; }
if (polyScalars) { const scalarValue = [...color];
if (inputScalars) { inputScalars.getTuple(cellId, scalarValue); }
const n = numPts - 3; const m = polyScalars.getNumberOfTuples(); if (n >= 0) { polyScalars.insertTuple(m + n, scalarValue);
for (let k = 0; k < n; k++) { polyScalars.setTuple(m + k, scalarValue); } } } } }
function triangulateContours( polyData, firstLine, numLines, outputPolys, normal ) { if (numLines <= 0) { return; }
const triangulationError = !vtkContourTriangulator.triangulateContours( polyData, firstLine, numLines, outputPolys, [-normal[0], -normal[1], -normal[2]] );
if (triangulationError && model.triangulationErrorDisplay) { vtkErrorMacro('Triangulation failed, polyData may not be watertight.'); } }
function triangulatePolygon(polygon, points, triangles) { return vtkContourTriangulator.triangulatePolygon( polygon, points, triangles ); }
function clipAndContourPolys( points, pointScalars, pointData, edgeLocator, triangulate, inputPolys, outputPolys, outputLines, inCellData, outPolyData, outLineData ) { const idList = model._idList; let polyMax = Number.MAX_VALUE; if (triangulate) { if (triangulate < 4) { polyMax = 3; } else if (triangulate === 4) { polyMax = 4; } }
let triangulationFailure = false;
const values = inputPolys.getData(); const linePts = []; let cellId = 0; let numPts; for (let i = 0; i < values.length; i += numPts + 1, cellId++) { numPts = values[i];
let i1 = values[i + numPts]; let v1 = pointScalars.getData()[i1]; let c1 = v1 > 0;
let j0 = c1 ? i1 : -1; let j1 = 0;
linePts[0] = 0; linePts[1] = 0;
let idListIdx = 0; for (let j = 1; j <= numPts; j++) { const i0 = i1; const v0 = v1; const c0 = c1;
i1 = values[i + j]; v1 = pointScalars.getData()[i1]; c1 = v1 > 0;
if (c0 || c1) { if (c0 ? !c1 : c1) { j1 = interpolateEdge( points, pointData, edgeLocator, model.tolerance, i0, i1, v0, v1 );
if (j1 !== j0) { idList[idListIdx++] = j1; j0 = j1; }
linePts[c0 ? 1 : 0] = j1; }
if (c1) { j1 = i1;
if (j1 !== j0) { idList[idListIdx++] = j1; j0 = j1; } } } }
const numPoints = idListIdx; idList.length = numPoints;
if (model.triangulatePolys && numPoints > polyMax) { let newCellId = outputPolys.getNumberOfCells(); const success = triangulatePolygon(idList, points, outputPolys); if (!success) { triangulationFailure = true; }
const ncells = outputPolys.getNumberOfCells(); for (; newCellId < ncells; newCellId++) { outPolyData.passData(inCellData, cellId, newCellId); } } else if (numPoints > 2) { const newCellId = outputPolys.insertNextCell(idList); outPolyData.passData(inCellData, cellId, newCellId); }
if (linePts[0] !== linePts[1]) { const newCellId = outputLines.insertNextCell(linePts); outLineData.passData(inCellData, cellId, newCellId); } }
if (triangulationFailure && model.triangulationErrorDisplay) { vtkErrorMacro('Triangulation failed, output may not be watertight'); } }
function squeezeOutputPoints(output, points, pointData, outputPointDataType) { const n = points.getNumberOfPoints(); let numNewPoints = 0;
const outPointData = output.getPointData(); const pointMap = []; pointMap.length = n;
const cellArrays = [ output.getVerts(), output.getLines(), output.getPolys(), output.getStrips(), ];
cellArrays.forEach((cellArray) => { if (!cellArray) { return; } const values = cellArray.getData(); let numPts; let pointId; for (let i = 0; i < values.length; i += numPts + 1) { numPts = values[i]; for (let j = 1; j <= numPts; j++) { pointId = values[i + j]; if (pointMap[pointId] === undefined) { pointMap[pointId] = numNewPoints++; } } } });
const newPoints = vtkPoints.newInstance({ size: numNewPoints * 3, dataType: outputPointDataType, });
const p = []; let newPointId; for (let pointId = 0; pointId < n; pointId++) { newPointId = pointMap[pointId]; if (newPointId !== undefined) { points.getPoint(pointId, p); newPoints.setTuple(newPointId, p); outPointData.passData(pointData, pointId, newPointId); } }
cellArrays.forEach((cellArray) => { if (!cellArray) { return; } const values = cellArray.getData(); let numPts; let pointId; for (let i = 0; i < values.length; i += numPts + 1) { numPts = values[i]; for (let j = 1; j <= numPts; j++) { pointId = values[i + j]; values[i + j] = pointMap[pointId]; } } });
output.setPoints(newPoints); }
publicAPI.requestData = (inData, outData) => { const input = inData[0]; const output = vtkPolyData.newInstance(); outData[0] = output;
if (!input) { vtkErrorMacro('Invalid or missing input'); return; }
if (model._idList == null) { model._idList = []; } else { model._idList.length = 0; }
const inputPoints = input.getPoints(); let numPts = 0; let inputPointsType = VtkDataTypes.FLOAT; if (inputPoints) { numPts = inputPoints.getNumberOfPoints(); inputPointsType = inputPoints.getDataType(); }
const points = vtkPoints.newInstance({ size: numPts * 3, dataType: VtkDataTypes.DOUBLE, });
const pointData = vtkDataSetAttributes.newInstance(); let inPointData = null;
if (model.passPointData) { inPointData = input.getPointData(); }
const point = []; for (let ptId = 0; ptId < numPts; ptId++) { inputPoints.getPoint(ptId, point); points.setTuple(ptId, point); if (inPointData) { pointData.passData(inPointData, ptId, ptId); } }
const edgeLocator = vtkEdgeLocator.newInstance();
const tmpContourData = vtkPolyData.newInstance();
let lineScalars; let polyScalars; let inputScalars;
let firstLineScalar = 0; let firstPolyScalar = 0; let firstStripScalar = 0;
let numberOfScalarComponents = 1; const colors = [ [0, 0, 0], [0, 0, 0], [0, 0, 0], ];
if (model.scalarMode === ScalarMode.COLORS) { numberOfScalarComponents = 3; createColorValues( model.baseColor, model.clipColor, model.activePlaneColor, colors ); } else if (model.scalarMode === ScalarMode.LABELS) { colors[0][0] = 0; colors[1][0] = 1; colors[2][0] = 2; }
const numVerts = input.getVerts()?.getNumberOfCells() || 0; const inputLines = input.getLines(); const numLines = inputLines?.getNumberOfCells() || 0; const inputPolys = input.getPolys(); const numPolys = inputPolys?.getNumberOfCells() || 0; const numStrips = input.getStrips()?.getNumberOfCells() || 0;
if (model.scalarMode !== ScalarMode.NONE) { lineScalars = vtkDataArray.newInstance({ dataType: VtkDataTypes.UNSIGNED_CHAR, empty: true, numberOfComponents: numberOfScalarComponents, });
const tryInputScalars = input.getCellData().getScalars(); if ( tryInputScalars && tryInputScalars.getDataType() === VtkDataTypes.UNSIGNED_CHAR && numberOfScalarComponents === 3 && tryInputScalars.getNumberOfComponents() === 3 ) { inputScalars = input.getCellData().getScalars(); firstLineScalar = numVerts; firstPolyScalar = numVerts + numLines; firstStripScalar = numVerts + numLines + numPolys; } }
let lines; if (numLines > 0) { lines = vtkCellArray.newInstance({ dataType: inputLines.getDataType(), values: new Uint8Array(numLines * 3), size: 0, }); breakPolylines( inputLines, lines, inputScalars, firstLineScalar, lineScalars, colors[0] ); } else { lines = vtkCellArray.newInstance({ empty: true, }); }
let polys = null; let polyMax = 3; if (numPolys > 0 || numStrips > 0) { if (lineScalars) { polyScalars = vtkDataArray.newInstance({ dataType: VtkDataTypes.UNSIGNED_CHAR, empty: true, numberOfComponents: numberOfScalarComponents, }); }
polys = vtkCellArray.newInstance(); copyPolygons( inputPolys, polys, inputScalars, firstPolyScalar, polyScalars, colors[0] ); breakTriangleStrips( input.getStrips(), polys, inputScalars, firstStripScalar, polyScalars, colors[0] );
polyMax = inputPolys.getCellSizes().reduce((a, b) => (a > b ? a : b), 0); }
let newLines = vtkCellArray.newInstance({ dataType: lines.getDataType(), empty: true, }); let newPolys = null; if (polys) { newPolys = vtkCellArray.newInstance({ dataType: polys.getDataType(), empty: true, }); }
let inLineData = vtkDataSetAttributes.newInstance(); inLineData.copyScalarsOn(); inLineData.setScalars(lineScalars);
let inPolyData = vtkDataSetAttributes.newInstance(); inPolyData.copyScalarsOn(); inPolyData.setScalars(polyScalars);
let outLineData = vtkDataSetAttributes.newInstance(); outLineData.copyScalarsOn();
let outPolyData = vtkDataSetAttributes.newInstance(); outPolyData.copyScalarsOn();
const planes = model.clippingPlanes;
for (let planeId = 0; planeId < planes.length; planeId++) { const plane = planes[planeId];
let triangulate = 5; if (planeId === planes.length - 1) { triangulate = polyMax; }
const active = planeId === model.activePlaneId;
const pc = plane.getNormal(); pc[3] = -vtkMath.dot(pc, plane.getOrigin());
const numPoints = points.getNumberOfPoints();
const pointScalars = vtkDataArray.newInstance({ dataType: VtkDataTypes.DOUBLE, size: numPoints, }); const pointScalarsData = pointScalars.getData(); const pointsData = points.getData(); let i = 0; for (let pointId = 0; pointId < numPoints; pointId) { pointScalarsData[pointId++] = pointsData[i++] * pc[0] + pointsData[i++] * pc[1] + pointsData[i++] * pc[2] + pc[3]; }
edgeLocator.initialize();
clipLines( points, pointScalars, pointData, edgeLocator, lines, newLines, inLineData, outLineData );
if (polys) { const numClipLines = newLines.getNumberOfCells();
clipAndContourPolys( points, pointScalars, pointData, edgeLocator, triangulate, polys, newPolys, newLines, inPolyData, outPolyData, outLineData );
let scalars = outLineData.getScalars();
if (scalars) { const color = colors[1 + (active ? 1 : 0)]; const activeColor = colors[2]; const numNewLines = newLines.getNumberOfCells();
const oldColor = []; for (let lineId = numClipLines; lineId < numNewLines; lineId++) { scalars.getTuple(lineId, oldColor); if ( numberOfScalarComponents !== 3 || oldColor[0] !== activeColor[0] || oldColor[1] !== activeColor[1] || oldColor[2] !== activeColor[2] ) { scalars.setTuple(lineId, color); } } }
let cellId = newPolys.getNumberOfCells(); const numClipAndContourLines = newLines.getNumberOfCells();
tmpContourData.setPoints(points); tmpContourData.setLines(newLines); tmpContourData.buildCells();
triangulateContours( tmpContourData, numClipLines, numClipAndContourLines - numClipLines, newPolys, pc );
scalars = outPolyData.getScalars();
if (scalars) { const color = colors[1 + (active ? 1 : 0)];
const numCells = newPolys.getNumberOfCells(); if (numCells > cellId) { scalars.insertTuple(numCells - 1, color); for (; cellId < numCells; cellId++) { scalars.setTuple(cellId, color); } } }
scalars = outLineData.getScalars();
if (scalars) { const color = [0, 255, 255]; const numCells = newLines.getNumberOfCells(); if (numCells > numClipAndContourLines) { scalars.insertTuple(numCells - 1, color); for ( let lineCellId = numClipAndContourLines; lineCellId < numCells; lineCellId++ ) { scalars.setTuple(lineCellId, color); } } } }
[lines, newLines] = [newLines, lines]; newLines.initialize();
if (polys) { [polys, newPolys] = [newPolys, polys]; newPolys.initialize(); }
[inLineData, outLineData] = [outLineData, inLineData]; outLineData.initialize();
[inPolyData, outPolyData] = [outPolyData, inPolyData]; outPolyData.initialize(); }
const scalars = inLineData.getScalars();
if (model.generateOutline) { output.setLines(lines); } else if (scalars) { scalars.initialize(); }
if (model.generateFaces) { output.setPolys(polys);
if (polys && scalars) { const pScalars = inPolyData.getScalars(); const m = scalars.getNumberOfTuples(); const n = pScalars.getNumberOfTuples();
if (n > 0) { const color = [0, 0, 0];
scalars.insertTuple(n + m - 1, color);
for (let i = 0; i < n; i++) { pScalars.getTuple(i, color); scalars.setTuple(i + m, color); } } } }
if (scalars && model.scalarMode === ScalarMode.COLORS) { scalars.setName('Colors'); output.getCellData().setScalars(scalars); } else if (model.scalarMode === ScalarMode.LABELS) { const categories = scalars.newClone(); categories.setData(scalars.getData().slice()); categories.setName('Labels'); output.getCellData().setScalars(categories); } else { output.getCellData().setScalars(null); }
squeezeOutputPoints(output, points, pointData, inputPointsType); outData[0] = output; };
Object.keys(ScalarMode).forEach((key) => { const name = capitalize(key.toLowerCase()); publicAPI[`setScalarModeTo${name}`] = () => { model.scalarMode = ScalarMode[key]; }; }); }
const DEFAULT_VALUES = { clippingPlanes: null, tolerance: 1e-6, passPointData: false, triangulatePolys: false,
scalarMode: ScalarMode.NONE, generateOutline: false, generateFaces: true, activePlaneId: -1,
baseColor: [255 / 255, 99 / 255, 71 / 255], clipColor: [244 / 255, 164 / 255, 96 / 255], activePlaneColor: [227 / 255, 207 / 255, 87 / 255],
triangulationErrorDisplay: false, };
export function extend(publicAPI, model, initialValues = {}) { Object.assign(model, DEFAULT_VALUES, initialValues);
macro.obj(publicAPI, model);
macro.algo(publicAPI, model, 1, 1);
macro.setGet(publicAPI, model, [ 'clippingPlanes', 'tolerance', 'passPointData', 'triangulatePolys', 'scalarMode', 'generateOutline', 'generateFaces', 'activePlaneId', 'triangulationErrorDisplay', ]);
macro.setGetArray( publicAPI, model, ['baseColor', 'clipColor', 'activePlaneColor'], 3 );
vtkClipClosedSurface(publicAPI, model); }
export const newInstance = macro.newInstance(extend, 'vtkClipClosedSurface');
export default { newInstance, extend, ...Constants };
|