diff --git a/Documentation/content/docs/gallery/CleanPolyData.jpg b/Documentation/content/docs/gallery/CleanPolyData.jpg new file mode 100644 index 00000000000..dc6d1030d3a Binary files /dev/null and b/Documentation/content/docs/gallery/CleanPolyData.jpg differ diff --git a/Documentation/content/examples/index.md b/Documentation/content/examples/index.md index 15badfea191..764f259be13 100644 --- a/Documentation/content/examples/index.md +++ b/Documentation/content/examples/index.md @@ -109,6 +109,7 @@ This will allow you to see the some live code running in your browser. Just pick [![PolyDataNormals Example][PolyDataNormals]](./PolyDataNormals.html "PolyDataNormals") [![ThresholdPoints Example][ThresholdPoints]](./ThresholdPoints.html "Cut/Treshold points with point data criteria") [![ShrinkPolyData Example][ShrinkPolyData]](./ShrinkPolyData.html "ShrinkPolyData") +[![CleanPolyData Example][CleanPolyData]](./CleanPolyData.html "CleanPolyData") @@ -129,6 +130,7 @@ This will allow you to see the some live code running in your browser. Just pick [PolyDataNormals]: ../docs/gallery/PolyDataNormals.jpg [ThresholdPoints]: ../docs/gallery/ThresholdPoints.jpg [ShrinkPolyData]: ../docs/gallery/ShrinkPolyData.jpg +[CleanPolyData]: ../docs/gallery/CleanPolyData.jpg # Sources diff --git a/Sources/Common/Core/CellArray/index.d.ts b/Sources/Common/Core/CellArray/index.d.ts index 17041c3ab90..f519b9f37e8 100755 --- a/Sources/Common/Core/CellArray/index.d.ts +++ b/Sources/Common/Core/CellArray/index.d.ts @@ -46,6 +46,11 @@ export interface vtkCellArray extends vtkDataArray { * @returns {Number} Idx of where the cell was inserted */ insertNextCell(cellPointIds: number[]): number; + + /** + * Get the maximum cell size. + */ + getMaxCellSize(): number; } /** diff --git a/Sources/Common/Core/CellArray/index.js b/Sources/Common/Core/CellArray/index.js index f86178bc006..ce1ce087f24 100644 --- a/Sources/Common/Core/CellArray/index.js +++ b/Sources/Common/Core/CellArray/index.js @@ -117,6 +117,9 @@ function vtkCellArray(publicAPI, model) { } return cellId; }; + + publicAPI.getMaxCellSize = () => + publicAPI.getCellSizes().reduce((a, b) => Math.max(a, b), 0); } // ---------------------------------------------------------------------------- diff --git a/Sources/Common/DataModel/BoundingBox/index.js b/Sources/Common/DataModel/BoundingBox/index.js index 485da08e5ae..652067e95fa 100644 --- a/Sources/Common/DataModel/BoundingBox/index.js +++ b/Sources/Common/DataModel/BoundingBox/index.js @@ -993,6 +993,7 @@ export const STATIC = { getLengths, getMaxLength, getDiagonalLength, + getDiagonalLength2, getMinPoint, getMaxPoint, getXRange, diff --git a/Sources/Common/DataModel/DataSet/index.js b/Sources/Common/DataModel/DataSet/index.js index 16a629b54dd..9418c832cac 100644 --- a/Sources/Common/DataModel/DataSet/index.js +++ b/Sources/Common/DataModel/DataSet/index.js @@ -1,39 +1,10 @@ import macro from 'vtk.js/Sources/macros'; import vtk from 'vtk.js/Sources/vtk'; +import vtkBoundingBox from 'vtk.js/Sources/Common/DataModel/BoundingBox'; import vtkDataSetAttributes from 'vtk.js/Sources/Common/DataModel/DataSetAttributes'; +import vtkMath from 'vtk.js/Sources/Common/Core/Math'; import Constants from 'vtk.js/Sources/Common/DataModel/DataSet/Constants'; -// import vtkBoundingBox from '../BoundingBox'; -// import * as vtkMath from '../../Core/Math'; -// -// function getBounds(dataset) { -// if (dataset.bounds) { -// return dataset.bounds; -// } -// if (dataset.type && dataset[dataset.type]) { -// const ds = dataset[dataset.type]; -// if (ds.bounds) { -// return ds.bounds; -// } -// if (ds.Points && ds.Points.bounds) { -// return ds.Points.bounds; -// } - -// if (ds.Points && ds.Points.values) { -// const array = ds.Points.values; -// const bbox = [...vtkBoundingBox.INIT_BOUNDS]; -// const size = array.length; -// const delta = ds.Points.numberOfComponents ? ds.Points.numberOfComponents : 3; -// for (let idx = 0; idx < size; idx += delta) { -// vtkBoundingBox.addPoint(bbox, array[idx * delta], array[(idx * delta) + 1], array[(idx * delta) + 2]); -// } -// ds.Points.bounds = bbox; -// return ds.Points.bounds; -// } -// } -// return vtkMath.createUninitializedBounds(); -// } - // ---------------------------------------------------------------------------- // Global methods // ---------------------------------------------------------------------------- @@ -57,6 +28,73 @@ function vtkDataSet(publicAPI, model) { } }); + //------------------------------------------------------------------------------ + // Compute the data bounding box from data points. + publicAPI.computeBounds = () => { + if ( + (model.modifiedTime && + model.computeTime && + model.modifiedTime > model.computeTime) || + !model.computeTime + ) { + const points = publicAPI.getPoints(); + if (points?.getNumberOfPoints()) { + // Compute bounds from points + vtkBoundingBox.setBounds(model.bounds, points.getBoundsByReference()); + } else { + model.bounds = vtkMath.createUninitializedBounds(); + } + // Update computeTime + model.computeTime = macro.getCurrentGlobalMTime(); + } + }; + + /** + * Returns the squared length of the diagonal of the bounding box + */ + publicAPI.getLength2 = () => { + const bounds = publicAPI.getBoundsByReference(); + if (!bounds || bounds.length !== 6) return 0; + return vtkBoundingBox.getDiagonalLength2(bounds); + }; + + /** + * Returns the length of the diagonal of the bounding box + */ + publicAPI.getLength = () => Math.sqrt(publicAPI.getLength2()); + + /** + * Returns the center of the bounding box as [x, y, z] + */ + publicAPI.getCenter = () => { + const bounds = publicAPI.getBoundsByReference(); + if (!bounds || bounds.length !== 6) return [0, 0, 0]; + return vtkBoundingBox.getCenter(bounds); + }; + + /** + * Get the bounding box of a cell with the given cellId + * @param {Number} cellId - The id of the cell + * @returns {Number[]} - The bounds as [xmin, xmax, ymin, ymax, zmin, zmax] + */ + publicAPI.getCellBounds = (cellId) => { + const cell = publicAPI.getCell(cellId); + if (cell) { + return cell.getBounds(); + } + return vtkMath.createUninitializedBounds(); + }; + + publicAPI.getBounds = macro.chain( + () => publicAPI.computeBounds, + publicAPI.getBounds + ); + + publicAPI.getBoundsByReference = macro.chain( + () => publicAPI.computeBounds, + publicAPI.getBoundsByReference + ); + const superShallowCopy = publicAPI.shallowCopy; publicAPI.shallowCopy = (other, debug = false) => { superShallowCopy(other, debug); @@ -98,7 +136,7 @@ export function extend(publicAPI, model, initialValues = {}) { // Object methods macro.obj(publicAPI, model); macro.setGet(publicAPI, model, DATASET_FIELDS); - + macro.getArray(publicAPI, model, ['bounds'], 6); // Object specific methods vtkDataSet(publicAPI, model); } diff --git a/Sources/Common/DataModel/DataSetAttributes/FieldData.js b/Sources/Common/DataModel/DataSetAttributes/FieldData.js index 350e4985172..8039843dacc 100644 --- a/Sources/Common/DataModel/DataSetAttributes/FieldData.js +++ b/Sources/Common/DataModel/DataSetAttributes/FieldData.js @@ -32,7 +32,7 @@ function vtkFieldData(publicAPI, model) { publicAPI.copyStructure = (other) => { publicAPI.initializeFields(); model.copyFieldFlags = other.getCopyFieldFlags().map((x) => x); // Deep-copy - model.arrays = other.arrays().map((x) => ({ array: x })); // Deep-copy + model.arrays = other.getArrays().map((x) => ({ data: x })); // Deep-copy // TODO: Copy array information objects (once we support information objects) }; diff --git a/Sources/Common/DataModel/PolyData/index.d.ts b/Sources/Common/DataModel/PolyData/index.d.ts index e11be352691..d364a3f6f15 100755 --- a/Sources/Common/DataModel/PolyData/index.d.ts +++ b/Sources/Common/DataModel/PolyData/index.d.ts @@ -66,9 +66,15 @@ export interface vtkPolyData extends vtkPointSet { getLines(): vtkCellArray; /** - * + * Get the links between points and cells. */ - getLinks(): any; + getLinks(): any; // vtkCellLinks + + /** + * Get the maximum cell size. + * Returns 0 if there is no cell. + */ + getMaxCellSize(): number; /** * Determine the number of cells composing the polydata. @@ -104,7 +110,7 @@ export interface vtkPolyData extends vtkPointSet { * Topological inquiry to get cells using point. * @param ptId */ - getPointCells(ptId: any): void; + getPointCells(ptId: number): void; /** * Get the cell array defining polys. diff --git a/Sources/Common/DataModel/PolyData/index.js b/Sources/Common/DataModel/PolyData/index.js index b81cf62cc66..be145e5c60e 100644 --- a/Sources/Common/DataModel/PolyData/index.js +++ b/Sources/Common/DataModel/PolyData/index.js @@ -5,8 +5,11 @@ import vtkCellLinks from 'vtk.js/Sources/Common/DataModel/CellLinks'; import vtkCellTypes from 'vtk.js/Sources/Common/DataModel/CellTypes'; import vtkLine from 'vtk.js/Sources/Common/DataModel/Line'; import vtkPointSet from 'vtk.js/Sources/Common/DataModel/PointSet'; +import vtkPolyLine from 'vtk.js/Sources/Common/DataModel/PolyLine'; +import vtkPolygon from 'vtk.js/Sources/Common/DataModel/Polygon'; +import vtkQuad from 'vtk.js/Sources/Common/DataModel/Quad'; import vtkTriangle from 'vtk.js/Sources/Common/DataModel/Triangle'; - +import vtkTriangleStrip from 'vtk.js/Sources/Common/DataModel/TriangleStrip'; import { CellType } from 'vtk.js/Sources/Common/DataModel/CellTypes/Constants'; import { POLYDATA_FIELDS } from 'vtk.js/Sources/Common/DataModel/PolyData/Constants'; @@ -14,8 +17,12 @@ const { vtkWarningMacro } = macro; export const CELL_FACTORY = { [CellType.VTK_LINE]: vtkLine, + [CellType.VTK_QUAD]: vtkQuad, [CellType.VTK_POLY_LINE]: vtkLine, [CellType.VTK_TRIANGLE]: vtkTriangle, + [CellType.VTK_TRIANGLE_STRIP]: vtkTriangleStrip, + [CellType.VTK_POLY_LINE]: vtkPolyLine, + [CellType.VTK_POLYGON]: vtkPolygon, }; // ---------------------------------------------------------------------------- @@ -242,6 +249,12 @@ function vtkPolyData(publicAPI, model) { cell.initialize(publicAPI.getPoints(), cellInfo.cellPointIds); return cell; }; + + publicAPI.getMaxCellSize = () => + POLYDATA_FIELDS.reduce( + (max, type) => Math.max(max, model[type]?.getMaxCellSize?.() ?? 0), + 0 + ); } // ---------------------------------------------------------------------------- diff --git a/Sources/Filters/Core/CleanPolyData/example/controlPanel.html b/Sources/Filters/Core/CleanPolyData/example/controlPanel.html new file mode 100644 index 00000000000..c0b4627394b --- /dev/null +++ b/Sources/Filters/Core/CleanPolyData/example/controlPanel.html @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + +
Before(Left Cube)
Points : 0Cells : 0Lines : 0Polys : 0Strips : 0
After(Right Cube)
Points : 0Cells : 0Lines : 0Polys : 0Strips : 0
diff --git a/Sources/Filters/Core/CleanPolyData/example/index.js b/Sources/Filters/Core/CleanPolyData/example/index.js new file mode 100644 index 00000000000..f5925d653c4 --- /dev/null +++ b/Sources/Filters/Core/CleanPolyData/example/index.js @@ -0,0 +1,116 @@ +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 '@kitware/vtk.js/Rendering/Profiles/Glyph'; + +import '@kitware/vtk.js/IO/Core/DataAccessHelper/HttpDataAccessHelper'; + +import vtkActor from '@kitware/vtk.js/Rendering/Core/Actor'; +import vtkCleanPolyData from '@kitware/vtk.js/Filters/Core/CleanPolyData'; +import vtkCubeSource from '@kitware/vtk.js/Filters/Sources/CubeSource'; +import vtkFullScreenRenderWindow from '@kitware/vtk.js/Rendering/Misc/FullScreenRenderWindow'; +import vtkMapper from '@kitware/vtk.js/Rendering/Core/Mapper'; +import vtkGlyph3DMapper from '@kitware/vtk.js/Rendering/Core/Glyph3DMapper'; +import vtkArrowSource from '@kitware/vtk.js/Filters/Sources/ArrowSource'; + +import controlPanel from './controlPanel.html'; + +// ---------------------------------------------------------------------------- +// Standard rendering code setup +// ---------------------------------------------------------------------------- + +const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance(); +const renderer = fullScreenRenderer.getRenderer(); +const renderWindow = fullScreenRenderer.getRenderWindow(); + +// ----------------------------------------------------------- +// UI control handling +// ----------------------------------------------------------- + +fullScreenRenderer.addController(controlPanel); + +// ---------------------------------------------------------------------------- +// Example code +// ---------------------------------------------------------------------------- + +const cubeSource1 = vtkCubeSource.newInstance(); +const cubeActor1 = vtkActor.newInstance(); +const cubeMapper1 = vtkMapper.newInstance(); +cubeActor1.setMapper(cubeMapper1); +cubeMapper1.setInputConnection(cubeSource1.getOutputPort()); +renderer.addActor(cubeActor1); + +const arrowSource1 = vtkArrowSource.newInstance(); +const glyphMapper1 = vtkGlyph3DMapper.newInstance(); +glyphMapper1.setInputConnection(cubeSource1.getOutputPort()); +glyphMapper1.setSourceConnection(arrowSource1.getOutputPort()); +glyphMapper1.setOrientationModeToDirection(); +glyphMapper1.setOrientationArray('Normals'); +glyphMapper1.setScaleModeToScaleByMagnitude(); +glyphMapper1.setScaleArray('Normals'); +glyphMapper1.setScaleFactor(0.1); + +const glyphActor1 = vtkActor.newInstance(); +glyphActor1.setMapper(glyphMapper1); +renderer.addActor(glyphActor1); + +const cubeSource2 = vtkCubeSource.newInstance(); +const cubeActor2 = vtkActor.newInstance(); +const cubeMapper2 = vtkMapper.newInstance(); + +cubeActor2.setMapper(cubeMapper2); +cubeMapper2.setInputConnection(cubeSource2.getOutputPort()); +cubeActor2.setPosition(2, 0, 0); +renderer.addActor(cubeActor2); + +const cleanPolyData = vtkCleanPolyData.newInstance(); +cleanPolyData.setInputConnection(cubeSource2.getOutputPort()); + +const arrowSource2 = vtkArrowSource.newInstance(); +const glyphMapper2 = vtkGlyph3DMapper.newInstance(); +glyphMapper2.setInputConnection(cleanPolyData.getOutputPort()); +glyphMapper2.setSourceConnection(arrowSource2.getOutputPort()); +glyphMapper2.setOrientationModeToDirection(); +glyphMapper2.setOrientationArray('Normals'); +glyphMapper2.setScaleModeToScaleByMagnitude(); +glyphMapper2.setScaleArray('Normals'); +glyphMapper2.setScaleFactor(0.1); + +const glyphActor2 = vtkActor.newInstance(); +glyphActor2.setMapper(glyphMapper2); +glyphActor2.setPosition(2, 0, 0); +renderer.addActor(glyphActor2); + +// --- Render --- +renderer.resetCamera(); +renderWindow.render(); + +// ----------------------------------------------------------- +// Display initial and final polydata stats +// ----------------------------------------------------------- +const initialPolyData = cubeSource1.getOutputData(); +const initialPoints = initialPolyData.getNumberOfPoints(); +const initialCells = initialPolyData.getNumberOfCells(); +const initialLines = initialPolyData.getLines().getNumberOfCells(); +const initialPolys = initialPolyData.getPolys().getNumberOfCells(); +const initialStrips = initialPolyData.getStrips().getNumberOfCells(); + +document.querySelector('.initial-points').textContent = initialPoints; +document.querySelector('.initial-cells').textContent = initialCells; +document.querySelector('.initial-lines').textContent = initialLines; +document.querySelector('.initial-polys').textContent = initialPolys; +document.querySelector('.initial-strips').textContent = initialStrips; + +const finalPolyData = cleanPolyData.getOutputData(); +const finalPoints = finalPolyData.getNumberOfPoints(); +const finalCells = finalPolyData.getNumberOfCells(); +const finalLines = finalPolyData.getLines().getNumberOfCells(); +const finalPolys = finalPolyData.getPolys().getNumberOfCells(); +const finalStrips = finalPolyData.getStrips().getNumberOfCells(); + +document.querySelector('.final-points').textContent = finalPoints; +document.querySelector('.final-cells').textContent = finalCells; +document.querySelector('.final-lines').textContent = finalLines; +document.querySelector('.final-polys').textContent = finalPolys; +document.querySelector('.final-strips').textContent = finalStrips; diff --git a/Sources/Filters/Core/CleanPolyData/index.d.ts b/Sources/Filters/Core/CleanPolyData/index.d.ts new file mode 100644 index 00000000000..ec40d3c3691 --- /dev/null +++ b/Sources/Filters/Core/CleanPolyData/index.d.ts @@ -0,0 +1,208 @@ +import { DesiredOutputPrecision } from '../../../Common/DataModel/DataSetAttributes'; +import { vtkAlgorithm, vtkObject } from '../../../interfaces'; +import { Bounds, Vector3 } from '../../../types'; + +/** + * Initial values for vtkCleanPolyData. + */ +export interface ICleanPolyDataInitialValues { + /** + * The tolerance used for point merging. + */ + tolerance?: number; + + /** + * Whether the tolerance is absolute or relative. + */ + toleranceIsAbsolute?: boolean; + + /** + * The absolute tolerance value. + */ + absoluteTolerance?: number; + + /** + * The desired output precision for points. + */ + outputPointsPrecision?: DesiredOutputPrecision; + + /** + * Whether to merge points. + */ + pointMerging?: boolean; + + /** + * Whether to convert lines to points. + */ + convertLinesToPoints?: boolean; + + /** + * Whether to convert polygons to lines. + */ + convertPolysToLines?: boolean; + + /** + * Whether to convert strips to polygons. + */ + convertStripsToPolys?: boolean; +} + +type vtkCleanPolyDataBase = vtkObject & vtkAlgorithm; + +export interface vtkCleanPolyData extends vtkCleanPolyDataBase { + /** + * Create default locator. + */ + createDefaultLocator(): void; + + /** + * Get the absolute tolerance value. + */ + getAbsoluteTolerance(): number; + + /** + * Get whether to convert lines to points. + */ + getConvertLinesToPoints(): boolean; + + /** + * Get whether to convert polygons to lines. + */ + getConvertPolysToLines(): boolean; + + /** + * Get whether to convert strips to polygons. + */ + getConvertStripsToPolys(): boolean; + + /** + * Get the output points precision. + */ + getOutputPointsPrecision(): DesiredOutputPrecision; + + /** + * Get whether to merge points. + */ + getPointMerging(): boolean; + + /** + * Get the tolerance used for point merging. + */ + getTolerance(): number; + + /** + * Get whether the tolerance is absolute or relative. + */ + getToleranceIsAbsolute(): boolean; + + /** + * Operate on a bounding box by applying a transformation. + * + * @param {Bounds} inBounds The input bounding box. + * @param {Bounds} outBounds The output bounding box. + */ + operateOnBounds(inBounds: Bounds, outBounds: Bounds): void; + + /** + * Operate on a point by applying a transformation. + * + * @param {Vector3} point The point to operate on. + */ + operateOnPoint(point: Vector3): void; + + /** + * + * @param inData + * @param outData + */ + requestData(inData: any, outData: any): void; + + /** + * Set the absolute tolerance value. + * This is only used if ToleranceIsAbsolute is true. + * Initial value is 0.0 + * @param {Number} absoluteTolerance The absolute tolerance value. + */ + setAbsoluteTolerance(absoluteTolerance: number): boolean; + + /** + * Set whether to convert lines to points. + * @param {Boolean} convertLinesToPoints + */ + setConvertLinesToPoints(convertLinesToPoints: boolean): boolean; + + /** + * Set whether to convert polygons to lines. + * @param {Boolean} convertPolysToLines + */ + setConvertPolysToLines(convertPolysToLines: boolean): boolean; + + /** + * Set whether to convert strips to polygons. + * @param {Boolean} convertStripsToPolys + */ + setConvertStripsToPolys(convertStripsToPolys: boolean): boolean; + + /** + * Set the desired output precision for points. + * Initial value is DEFAULT_PRECISION. + * @param {DesiredOutputPrecision} outputPointsPrecision The outputPointsPrecision value. + */ + setOutputPointsPrecision( + outputPointsPrecision: DesiredOutputPrecision + ): boolean; + + /** + * Set whether to merge points. + * Initial value is false. + * @param {Boolean} pointMerging The pointMerging value. + */ + setPointMerging(pointMerging: boolean): boolean; + + /** + * Set the tolerance used for point merging. + * This is ignored if ToleranceIsAbsolute is true. + * Initial value is 0.0 + * @param {Number} tolerance The tolerance value. + */ + setTolerance(tolerance: number): boolean; + + /** + * Set whether the tolerance is absolute or relative. + * Initial value is false (relative). + * @param {Boolean} toleranceIsAbsolute The toleranceIsAbsolute value. + */ + setToleranceIsAbsolute(toleranceIsAbsolute: boolean): boolean; +} + +/** + * Method used to decorate a given object (publicAPI+model) with vtkCleanPolyData characteristics. + * + * @param publicAPI object on which methods will be bounds (public) + * @param model object on which data structure will be bounds (protected) + * @param {ICleanPolyDataInitialValues} [initialValues] (default: {}) + */ +export function extend( + publicAPI: object, + model: object, + initialValues?: ICleanPolyDataInitialValues +): void; + +/** + * Method used to create a new instance of vtkCleanPolyData. + * @param {ICleanPolyDataInitialValues} [initialValues] for pre-setting some of its content + */ +export function newInstance( + initialValues?: ICleanPolyDataInitialValues +): vtkCleanPolyData; + +/** + * vtkCleanPolyData merge exactly coincident points. + * + * vtkCleanPolyData is a locator object to quickly locate points in 3D. + */ +export declare const vtkCleanPolyData: { + newInstance: typeof newInstance; + extend: typeof extend; +}; +export default vtkCleanPolyData; diff --git a/Sources/Filters/Core/CleanPolyData/index.js b/Sources/Filters/Core/CleanPolyData/index.js new file mode 100644 index 00000000000..0e54bdad102 --- /dev/null +++ b/Sources/Filters/Core/CleanPolyData/index.js @@ -0,0 +1,486 @@ +import macro from 'vtk.js/Sources/macros'; +import vtkBoundingBox from 'vtk.js/Sources/Common/DataModel/BoundingBox'; +import vtkCellArray from 'vtk.js/Sources/Common/Core/CellArray'; +import vtkMergePoints from 'vtk.js/Sources/Common/DataModel/MergePoints'; +import vtkPointLocator from 'vtk.js/Sources/Common/DataModel/PointLocator'; +import vtkPoints from 'vtk.js/Sources/Common/Core/Points'; +import vtkPolyData from 'vtk.js/Sources/Common/DataModel/PolyData/index'; +import { DesiredOutputPrecision } from 'vtk.js/Sources/Common/DataModel/DataSetAttributes/Constants'; +import { VtkDataTypes } from 'vtk.js/Sources/Common/Core/DataArray/Constants'; + +// ---------------------------------------------------------------------------- +// vtkCleanPolyData methods +// ---------------------------------------------------------------------------- + +function vtkCleanPolyData(publicAPI, model) { + // Set our classname + model.classHierarchy.push('vtkCleanPolyData'); + + const tempX = []; + + // Point processing + function processPoint( + ptId, + inPts, + newPts, + inputPD, + outputPD, + pointMap, + numUsedPts + ) { + const newX = [0, 0, 0]; + + inPts.getPoint(ptId, tempX); + publicAPI.operateOnPoint(tempX, newX); + + if (!model.pointMerging) { + if (pointMap[ptId] === -1) { + pointMap[ptId] = numUsedPts.value++; + newPts.setPoint(pointMap[ptId], newX); + outputPD.passData(inputPD, ptId, pointMap[ptId]); + } + return pointMap[ptId]; + } + const newPtId = model._locator.insertUniquePoint(newX).id; + if (!model.copiedPoints.has(newPtId)) { + model.copiedPoints.add(newPtId); + outputPD.passData(inputPD, ptId, newPtId); + } + return newPtId; + } + + publicAPI.operateOnPoint = (inPt, outPt) => { + outPt[0] = inPt[0]; + outPt[1] = inPt[1]; + outPt[2] = inPt[2]; + }; + + publicAPI.operateOnBounds = (inBounds, outBounds) => { + vtkBoundingBox.setBounds(outBounds, inBounds); + }; + + publicAPI.createDefaultLocator = (input) => { + let tol; + if (model.toleranceIsAbsolute) { + tol = model.absoluteTolerance; + } else if (input) { + tol = model.tolerance * input.getLength(); + } else { + tol = model.tolerance; + } + + if (!model._locator) { + model._locator = + tol === 0.0 + ? vtkMergePoints.newInstance() + : vtkPointLocator.newInstance(); + return; + } + + if (tol === 0.0 && model._locator?.getTolerance() !== 0.0) { + model._locator = vtkMergePoints.newInstance(); + } else if (tol > 0.0 && !(model._locator?.getTolerance() > 0.0)) { + model._locator = vtkPointLocator.newInstance(); + } + }; + + publicAPI.requestData = (inData, outData) => { + const input = inData[0]; + const output = outData[0]?.initialize() || vtkPolyData.newInstance(); + outData[0] = output; + + const inPts = input.getPoints(); + const numPts = input.getNumberOfPoints(); + + if (!inPts || numPts < 1) { + return; + } + + const updatedPts = new Array(input.getMaxCellSize()); + const numUsedPts = { value: 0 }; + + const precision = model.outputPointsPrecision; + let pointType = inPts.getDataType(); + if (precision) { + pointType = + precision === DesiredOutputPrecision.DOUBLE + ? VtkDataTypes.DOUBLE + : VtkDataTypes.FLOAT; + } + const newPts = vtkPoints.newInstance({ dataType: pointType }); + const inVerts = input.getVerts(); + const inLines = input.getLines(); + const inPolys = input.getPolys(); + const inStrips = input.getStrips(); + + let newVerts = null; + let newLines = null; + let newPolys = null; + let newStrips = null; + + const inputPD = input.getPointData(); + const inputCD = input.getCellData(); + const outputPD = output.getPointData(); + const outputCD = output.getCellData(); + + let pointMap = null; + if (model.pointMerging) { + publicAPI.createDefaultLocator(input); + + if (model.toleranceIsAbsolute) { + model._locator.setTolerance(model.absoluteTolerance); + } else { + model._locator.setTolerance(model.tolerance * input.getLength()); + } + + const originalBounds = input.getBounds(); + const mappedBounds = []; + publicAPI.operateOnBounds(originalBounds, mappedBounds); + model._locator.initPointInsertion(newPts, mappedBounds); + } else { + pointMap = new Array(numPts).fill(-1); + } + + // Copy data attributes setup + outputPD.copyStructure(inputPD); + outputCD.copyStructure(inputCD); + + model.copiedPoints.clear(); + + let outLineData = null; + let outPolyData = null; + let outStrpData = null; + let vertIDcounter = 0; + let lineIDcounter = 0; + let polyIDcounter = 0; + let strpIDcounter = 0; + + // Process vertices + let inCellID = 0; + if (inVerts && inVerts.getNumberOfCells() > 0) { + newVerts = vtkCellArray.newInstance(); + + let currentIdx = 0; + const cellData = inVerts.getData(); + while (currentIdx < cellData.length) { + const npts = cellData[currentIdx++]; + const inputPointIds = cellData.slice(currentIdx, currentIdx + npts); + currentIdx += npts; + + let numNewPts = 0; + + for (let i = 0; i < inputPointIds.length; i++) { + const ptId = inputPointIds[i]; + const newPtId = processPoint( + ptId, + inPts, + newPts, + inputPD, + outputPD, + pointMap, + numUsedPts + ); + updatedPts[numNewPts++] = newPtId; + } + + if (numNewPts > 0) { + newVerts.insertNextCell(updatedPts.slice(0, numNewPts)); + outputCD.passData(inputCD, inCellID, vertIDcounter); + vertIDcounter++; + } + inCellID++; + } + } + + // Process lines + if (inLines && inLines.getNumberOfCells() > 0) { + newLines = vtkCellArray.newInstance(); + + let currentIdx = 0; + const cellData = inLines.getData(); + while (currentIdx < cellData.length) { + const npts = cellData[currentIdx++]; + const inputPointIds = cellData.slice(currentIdx, currentIdx + npts); + currentIdx += npts; + + let numNewPts = 0; + + for (let i = 0; i < inputPointIds.length; i++) { + const ptId = inputPointIds[i]; + const newPtId = processPoint( + ptId, + inPts, + newPts, + inputPD, + outputPD, + pointMap, + numUsedPts + ); + + if (i === 0 || newPtId !== updatedPts[numNewPts - 1]) { + updatedPts[numNewPts++] = newPtId; + } + } + + if (numNewPts >= 2) { + newLines.insertNextCell(updatedPts.slice(0, numNewPts)); + if (!outLineData) { + outLineData = []; + } + outLineData.push({ inputId: inCellID, outputId: lineIDcounter }); + lineIDcounter++; + } else if ( + numNewPts === 1 && + (inputPointIds.length === numNewPts || model.convertLinesToPoints) + ) { + if (!newVerts) { + newVerts = vtkCellArray.newInstance(); + } + newVerts.insertNextCell(updatedPts.slice(0, numNewPts)); + outputCD.passData(inputCD, inCellID, vertIDcounter); + vertIDcounter++; + } + inCellID++; + } + } + + // Process polygons + if (inPolys && inPolys.getNumberOfCells() > 0) { + newPolys = vtkCellArray.newInstance(); + + let currentIdx = 0; + const cellData = inPolys.getData(); + while (currentIdx < cellData.length) { + const npts = cellData[currentIdx++]; + const inputPointIds = cellData.slice(currentIdx, currentIdx + npts); + currentIdx += npts; + + let numNewPts = 0; + + for (let i = 0; i < inputPointIds.length; i++) { + const ptId = inputPointIds[i]; + const newPtId = processPoint( + ptId, + inPts, + newPts, + inputPD, + outputPD, + pointMap, + numUsedPts + ); + + if (i === 0 || newPtId !== updatedPts[numNewPts - 1]) { + updatedPts[numNewPts++] = newPtId; + } + } + + // Remove duplicate last point if it matches first + if (numNewPts > 2 && updatedPts[0] === updatedPts[numNewPts - 1]) { + numNewPts--; + } + + if (numNewPts > 2) { + newPolys.insertNextCell(updatedPts.slice(0, numNewPts)); + if (!outPolyData) { + outPolyData = []; + } + outPolyData.push({ inputId: inCellID, outputId: polyIDcounter }); + polyIDcounter++; + } else if ( + numNewPts === 2 && + (inputPointIds.length === numNewPts || model.convertPolysToLines) + ) { + if (!newLines) { + newLines = vtkCellArray.newInstance(); + outLineData = []; + } + newLines.insertNextCell(updatedPts.slice(0, numNewPts)); + outLineData.push({ inputId: inCellID, outputId: lineIDcounter }); + lineIDcounter++; + } else if ( + numNewPts === 1 && + (inputPointIds.length === numNewPts || model.convertLinesToPoints) + ) { + if (!newVerts) { + newVerts = vtkCellArray.newInstance(); + } + newVerts.insertNextCell(updatedPts.slice(0, numNewPts)); + outputCD.passData(inputCD, inCellID, vertIDcounter); + vertIDcounter++; + } + inCellID++; + } + } + + // Process triangle strips + if (inStrips && inStrips.getNumberOfCells() > 0) { + newStrips = vtkCellArray.newInstance(); + + let currentIdx = 0; + const cellData = inStrips.getData(); + while (currentIdx < cellData.length) { + const npts = cellData[currentIdx++]; + const inputPointIds = cellData.slice(currentIdx, currentIdx + npts); + currentIdx += npts; + + let numNewPts = 0; + + for (let i = 0; i < inputPointIds.length; i++) { + const ptId = inputPointIds[i]; + const newPtId = processPoint( + ptId, + inPts, + newPts, + inputPD, + outputPD, + pointMap, + numUsedPts + ); + + if (i === 0 || newPtId !== updatedPts[numNewPts - 1]) { + updatedPts[numNewPts++] = newPtId; + } + } + + // Remove duplicate last point if it matches first + if (numNewPts > 1 && updatedPts[0] === updatedPts[numNewPts - 1]) { + numNewPts--; + } + + if (numNewPts > 3) { + newStrips.insertNextCell(updatedPts.slice(0, numNewPts)); + if (!outStrpData) { + outStrpData = []; + } + outStrpData.push({ inputId: inCellID, outputId: strpIDcounter }); + strpIDcounter++; + } else if ( + numNewPts === 3 && + (inputPointIds.length === numNewPts || model.convertStripsToPolys) + ) { + if (!newPolys) { + newPolys = vtkCellArray.newInstance(); + outPolyData = []; + } + newPolys.insertNextCell(updatedPts.slice(0, numNewPts)); + outPolyData.push({ inputId: inCellID, outputId: polyIDcounter }); + polyIDcounter++; + } else if ( + numNewPts === 2 && + (inputPointIds.length === numNewPts || model.convertPolysToLines) + ) { + if (!newLines) { + newLines = vtkCellArray.newInstance(); + outLineData = []; + } + newLines.insertNextCell(updatedPts.slice(0, numNewPts)); + outLineData.push({ inputId: inCellID, outputId: lineIDcounter }); + lineIDcounter++; + } else if ( + numNewPts === 1 && + (inputPointIds.length === numNewPts || model.convertLinesToPoints) + ) { + if (!newVerts) { + newVerts = vtkCellArray.newInstance(); + } + newVerts.insertNextCell(updatedPts.slice(0, numNewPts)); + outputCD.passData(inputCD, inCellID, vertIDcounter); + vertIDcounter++; + } + inCellID++; + } + } + + // Clean up + if (model.pointMerging) { + model._locator.initialize(); + } else { + newPts.setNumberOfPoints(numUsedPts.value); + } + + // Copy cell data in correct order + let combinedCellID = vertIDcounter; + + if (outLineData) { + outLineData.forEach((item) => { + outputCD.passData(inputCD, item.inputId, combinedCellID); + combinedCellID++; + }); + } + + if (outPolyData) { + outPolyData.forEach((item) => { + outputCD.passData(inputCD, item.inputId, combinedCellID); + combinedCellID++; + }); + } + + if (outStrpData) { + outStrpData.forEach((item) => { + outputCD.passData(inputCD, item.inputId, combinedCellID); + combinedCellID++; + }); + } + + // Set output + output.setPoints(newPts); + if (newVerts) output.setVerts(newVerts); + if (newLines) output.setLines(newLines); + if (newPolys) output.setPolys(newPolys); + if (newStrips) output.setStrips(newStrips); + }; +} + +// ---------------------------------------------------------------------------- +// Object factory +// ---------------------------------------------------------------------------- + +const DEFAULT_VALUES = { + pointMerging: true, + toleranceIsAbsolute: false, + tolerance: 0.0, + absoluteTolerance: 1.0, + convertLinesToPoints: true, + convertPolysToLines: true, + convertStripsToPolys: true, + locator: null, + outputPointsPrecision: DesiredOutputPrecision.DEFAULT, +}; + +// ---------------------------------------------------------------------------- + +export function extend(publicAPI, model, initialValues = {}) { + Object.assign(model, DEFAULT_VALUES, initialValues); + + // Make this a VTK object + macro.obj(publicAPI, model); + + // Also make it an algorithm with one input and one output + macro.algo(publicAPI, model, 1, 1); + + // Generate macros for properties + macro.setGet(publicAPI, model, [ + 'pointMerging', + 'toleranceIsAbsolute', + 'tolerance', + 'absoluteTolerance', + 'convertPolysToLines', + 'convertLinesToPoints', + 'convertStripsToPolys', + 'outputPointsPrecision', + ]); + + // Internal state + model.copiedPoints = new Set(); + + // Object methods + vtkCleanPolyData(publicAPI, model); +} + +// ---------------------------------------------------------------------------- + +export const newInstance = macro.newInstance(extend, 'vtkCleanPolyData'); + +// ---------------------------------------------------------------------------- + +export default { newInstance, extend }; diff --git a/Sources/Filters/Core/CleanPolyData/test/testCleanPolyData.js b/Sources/Filters/Core/CleanPolyData/test/testCleanPolyData.js new file mode 100644 index 00000000000..7e9dcfab90a --- /dev/null +++ b/Sources/Filters/Core/CleanPolyData/test/testCleanPolyData.js @@ -0,0 +1,229 @@ +import test from 'tape'; +import vtkPoints from 'vtk.js/Sources/Common/Core/Points'; +import vtkCellArray from 'vtk.js/Sources/Common/Core/CellArray'; +import vtkPolyData from 'vtk.js/Sources/Common/DataModel/PolyData'; +import vtkCleanPolyData from 'vtk.js/Sources/Filters/Core/CleanPolyData'; + +function constructLines() { + const pts = vtkPoints.newInstance(); + pts.insertNextTuple([0, 0, 0]); + pts.insertNextTuple([1, 0, 0]); + pts.insertNextTuple([1, 1, 0]); + pts.insertNextTuple([0, 0, 0]); // repeated + + const lines = vtkCellArray.newInstance(); + lines.insertNextCell([0, 1]); // valid line + lines.insertNextCell([0, 0]); // degenerate → vertex + lines.insertNextCell([0, 3]); // repeated pts → vertex if merging + lines.insertNextCell([0, 1, 2]); // polyline + lines.insertNextCell([0, 1, 1]); // degenerate → line + lines.insertNextCell([0, 3, 0]); // cycling → vertex if merging + + const pd = vtkPolyData.newInstance(); + pd.setPoints(pts); + pd.setLines(lines); + + return pd; +} + +function constructPolys() { + const pts = vtkPoints.newInstance(); + pts.insertNextTuple([0, 0, 0]); + pts.insertNextTuple([1, 0, 0]); + pts.insertNextTuple([1, 1, 0]); + pts.insertNextTuple([1, 1, 1]); // unused + pts.insertNextTuple([0, 0, 0]); // repeated + pts.insertNextTuple([1, 0, 0]); // repeated + + const polys = vtkCellArray.newInstance(); + polys.insertNextCell([0, 1, 2]); // normal tri + polys.insertNextCell([0, 0, 0]); // degenerate → vertex + polys.insertNextCell([0, 1, 1]); // degenerate → line + polys.insertNextCell([0, 1, 5]); // repeated id → line if merging + polys.insertNextCell([0, 4, 0]); // vertex if merging + polys.insertNextCell([1, 1, 1, 1]); // quad→vertex + polys.insertNextCell([0, 1, 1, 0]); // quad→line + + const pd = vtkPolyData.newInstance(); + pd.setPoints(pts); + pd.setPolys(polys); + return pd; +} + +function constructStrips() { + const pts = vtkPoints.newInstance(); + pts.insertNextTuple([0, 0, 0]); + pts.insertNextTuple([1, 0, 0]); + pts.insertNextTuple([1, 1, 0]); + pts.insertNextTuple([0, 1, 0]); + pts.insertNextTuple([1, 1, 1]); // unused + pts.insertNextTuple([0, 0, 0]); // repeated + pts.insertNextTuple([1, 0, 0]); // repeated + pts.insertNextTuple([1, 1, 0]); // repeated + + const strips = vtkCellArray.newInstance(); + strips.insertNextCell([0, 1, 2, 3]); // normal strip + strips.insertNextCell([0, 1, 2, 2]); // tri if no merging + strips.insertNextCell([0, 1, 2, 7]); // repeated→tri if merging + strips.insertNextCell([0, 1, 1, 1]); // line + strips.insertNextCell([0, 0, 6, 5]); // line or tri + strips.insertNextCell([2, 2, 2, 2]); // vertex + strips.insertNextCell([0, 0, 0, 5]); // vertex or line + + const pd = vtkPolyData.newInstance(); + pd.setPoints(pts); + pd.setStrips(strips); + return pd; +} + +function runTest(clean, inputPD, expected, t) { + clean.setInputData(inputPD); + // clean.update(); + const out = clean.getOutputData(); + + t.equal( + out.getNumberOfPoints(), + expected.points, + `expected ${expected.points} points but got ${out.getNumberOfPoints()}` + ); + t.equal( + out.getNumberOfVerts(), + expected.verts, + `expected ${expected.verts} verts but got ${out.getNumberOfVerts()}` + ); + t.equal( + out.getNumberOfLines(), + expected.lines, + `expected ${expected.lines} lines but got ${out.getNumberOfLines()}` + ); + t.equal( + out.getNumberOfPolys(), + expected.polys, + `expected ${expected.polys} polys but got ${out.getNumberOfPolys()}` + ); + t.equal( + out.getNumberOfStrips(), + expected.strips, + `expected ${expected.strips} strips but got ${out.getNumberOfStrips()}` + ); +} + +test('vtkCleanPolyData: degenerate conversions without merging', (t) => { + const clean = vtkCleanPolyData.newInstance({ + pointMerging: false, + convertLinesToPoints: true, + convertPolysToLines: true, + convertStripsToPolys: true, + }); + + runTest( + clean, + constructLines(), + { points: 4, verts: 1, lines: 5, polys: 0, strips: 0 }, + t + ); + runTest( + clean, + constructPolys(), + { points: 5, verts: 2, lines: 3, polys: 2, strips: 0 }, + t + ); + runTest( + clean, + constructStrips(), + { points: 7, verts: 1, lines: 2, polys: 2, strips: 2 }, + t + ); + + t.end(); +}); + +test('vtkCleanPolyData: degenerate elimination without merging', (t) => { + const clean = vtkCleanPolyData.newInstance({ + pointMerging: false, + convertLinesToPoints: false, + convertPolysToLines: false, + convertStripsToPolys: false, + }); + + runTest( + clean, + constructLines(), + { points: 4, verts: 0, lines: 5, polys: 0, strips: 0 }, + t + ); + runTest( + clean, + constructPolys(), + { points: 5, verts: 0, lines: 0, polys: 2, strips: 0 }, + t + ); + runTest( + clean, + constructStrips(), + { points: 7, verts: 0, lines: 0, polys: 0, strips: 2 }, + t + ); + + t.end(); +}); + +test('vtkCleanPolyData: degenerate conversions with merging', (t) => { + const clean = vtkCleanPolyData.newInstance({ + pointMerging: true, + convertLinesToPoints: true, + convertPolysToLines: true, + convertStripsToPolys: true, + }); + + runTest( + clean, + constructLines(), + { points: 3, verts: 3, lines: 3, polys: 0, strips: 0 }, + t + ); + runTest( + clean, + constructPolys(), + { points: 3, verts: 3, lines: 3, polys: 1, strips: 0 }, + t + ); + runTest( + clean, + constructStrips(), + { points: 4, verts: 2, lines: 2, polys: 2, strips: 1 }, + t + ); + + t.end(); +}); + +test('vtkCleanPolyData: degenerate elimination with merging', (t) => { + const clean = vtkCleanPolyData.newInstance({ + pointMerging: true, + convertLinesToPoints: false, + convertPolysToLines: false, + convertStripsToPolys: false, + }); + + runTest( + clean, + constructLines(), + { points: 3, verts: 0, lines: 3, polys: 0, strips: 0 }, + t + ); + runTest( + clean, + constructPolys(), + { points: 3, verts: 0, lines: 0, polys: 1, strips: 0 }, + t + ); + runTest( + clean, + constructStrips(), + { points: 4, verts: 0, lines: 0, polys: 0, strips: 1 }, + t + ); + + t.end(); +}); diff --git a/Sources/Filters/Core/index.js b/Sources/Filters/Core/index.js index d8663f345c3..1ab64b17774 100644 --- a/Sources/Filters/Core/index.js +++ b/Sources/Filters/Core/index.js @@ -1,7 +1,11 @@ +import vtkCleanPolyData from './CleanPolyData'; import vtkCutter from './Cutter'; import vtkPolyDataNormals from './PolyDataNormals'; +import vtkThresholdPoints from './ThresholdPoints'; export default { + vtkCleanPolyData, vtkCutter, vtkPolyDataNormals, + vtkThresholdPoints, };