From e505f4bc26fa5f343ece64c2179ceaa2c0f0dcd7 Mon Sep 17 00:00:00 2001 From: Andrew Maclean <andrew.amaclean@gmail.com> Date: Tue, 5 Nov 2024 14:00:11 +1100 Subject: [PATCH] Adding FileOutputWindow, FillHoles, IdentifyHoles --- src/Cxx.md | 2 +- src/Cxx/Meshes/FillHoles.cxx | 29 +- src/Cxx/Meshes/IdentifyHoles.cxx | 20 +- src/Cxx/Meshes/IdentifyHoles.md | 2 +- src/Cxx/Utilities/FileOutputWindow.md | 2 +- src/PythonicAPI.md | 5 +- src/PythonicAPI/Meshes/FillHoles.md | 6 + src/PythonicAPI/Meshes/FillHoles.py | 202 ++++++++++++++ src/PythonicAPI/Meshes/IdentifyHoles.md | 13 + src/PythonicAPI/Meshes/IdentifyHoles.py | 259 ++++++++++++++++++ src/PythonicAPI/Utilities/FileOutputWindow.md | 3 + src/PythonicAPI/Utilities/FileOutputWindow.py | 23 ++ .../Baseline/Cxx/Meshes/TestFillHoles.png | 4 +- .../PythonicAPI/Meshes/TestFillHoles.png | 3 + .../PythonicAPI/Meshes/TestIdentifyHoles.png | 3 + 15 files changed, 548 insertions(+), 28 deletions(-) create mode 100644 src/PythonicAPI/Meshes/FillHoles.md create mode 100644 src/PythonicAPI/Meshes/FillHoles.py create mode 100644 src/PythonicAPI/Meshes/IdentifyHoles.md create mode 100644 src/PythonicAPI/Meshes/IdentifyHoles.py create mode 100644 src/PythonicAPI/Utilities/FileOutputWindow.md create mode 100644 src/PythonicAPI/Utilities/FileOutputWindow.py create mode 100644 src/Testing/Baseline/PythonicAPI/Meshes/TestFillHoles.png create mode 100644 src/Testing/Baseline/PythonicAPI/Meshes/TestIdentifyHoles.png diff --git a/src/Cxx.md b/src/Cxx.md index 639680082ef..3f30d00b8d3 100644 --- a/src/Cxx.md +++ b/src/Cxx.md @@ -653,7 +653,7 @@ This section includes vtkUnstructuredGrid. [DetermineActorType](/Cxx/Utilities/DetermineActorType) | Determine the type of an actor. [DiscretizableColorTransferFunction](/Cxx/Utilities/DiscretizableColorTransferFunction) | Discretizable Color Transfer Function. [ExtractFaces](/Cxx/Utilities/ExtractFaces) | Extract faces froam vtkUnstructuredGrid. -[FileOutputWindow](/Cxx/Utilities/FileOutputWindow) | Write errors to a log file instead of the screen. +[FileOutputWindow](/Cxx/Utilities/FileOutputWindow) | Write errors to a log file instead of the usual vtk pop-up window. [FilenameFunctions](/Cxx/Utilities/FilenameFunctions) | Do things like get the file extension, strip the file extension, etc. [FilterSelfProgress](/Cxx/Developers/FilterSelfProgress) | Monitor a filters progress. [ForLoop](/Cxx/Utilities/ForLoop) | Demonstrating various ways of implementing a for loop in VTK. diff --git a/src/Cxx/Meshes/FillHoles.cxx b/src/Cxx/Meshes/FillHoles.cxx index 26918a5c679..25a75df4971 100644 --- a/src/Cxx/Meshes/FillHoles.cxx +++ b/src/Cxx/Meshes/FillHoles.cxx @@ -7,6 +7,7 @@ #include <vtkInformation.h> #include <vtkNamedColors.h> #include <vtkNew.h> +#include <vtkPointData.h> #include <vtkPolyData.h> #include <vtkPolyDataMapper.h> #include <vtkPolyDataNormals.h> @@ -29,11 +30,14 @@ #include <vtksys/SystemTools.hxx> namespace { +vtkSmartPointer<vtkPolyData> GenerateData(); vtkSmartPointer<vtkPolyData> ReadPolyData(const char* fileName); } int main(int argc, char* argv[]) { + auto restoreOriginalNormals = true; + vtkNew<vtkNamedColors> colors; auto input = ReadPolyData(argc > 1 ? argv[1] : ""); @@ -50,11 +54,13 @@ int main(int argc, char* argv[]) normals->SplittingOff(); normals->Update(); -#if 0 - // Restore the original normals - normals->GetOutput()->GetPointData()-> - SetNormals(input->GetPointData()->GetNormals()); -#endif + if (restoreOriginalNormals) + { + // Restore the original normals + normals->GetOutput()->GetPointData()->SetNormals( + input->GetPointData()->GetNormals()); + } + // Visualize // Define viewport ranges. // (xmin, ymin, xmax, ymax) @@ -75,7 +81,7 @@ int main(int argc, char* argv[]) colors->GetColor3d("NavajoWhite").GetData()); vtkNew<vtkPolyDataMapper> filledMapper; - filledMapper->SetInputData(fillHolesFilter->GetOutput()); + filledMapper->SetInputData(normals->GetOutput()); vtkNew<vtkActor> filledActor; filledActor->SetMapper(filledMapper); @@ -126,7 +132,9 @@ int main(int argc, char* argv[]) return EXIT_SUCCESS; } -void GenerateData(vtkPolyData* input) +namespace { + +vtkSmartPointer<vtkPolyData> GenerateData() { // Create a sphere. vtkNew<vtkSphereSource> sphereSource; @@ -160,10 +168,9 @@ void GenerateData(vtkPolyData* input) surfaceFilter->SetInputConnection(extractSelection->GetOutputPort()); surfaceFilter->Update(); - input->ShallowCopy(surfaceFilter->GetOutput()); + return surfaceFilter->GetOutput(); } -namespace { vtkSmartPointer<vtkPolyData> ReadPolyData(const char* fileName) { vtkSmartPointer<vtkPolyData> polyData; @@ -213,9 +220,7 @@ vtkSmartPointer<vtkPolyData> ReadPolyData(const char* fileName) } else { - vtkNew<vtkSphereSource> source; - source->Update(); - polyData = source->GetOutput(); + polyData = GenerateData(); } return polyData; } diff --git a/src/Cxx/Meshes/IdentifyHoles.cxx b/src/Cxx/Meshes/IdentifyHoles.cxx index 7cab1fd28d5..d52f2010578 100644 --- a/src/Cxx/Meshes/IdentifyHoles.cxx +++ b/src/Cxx/Meshes/IdentifyHoles.cxx @@ -38,7 +38,7 @@ int main(int argc, char* argv[]) fillHoles->SetInputConnection(reader->GetOutputPort()); fillHoles->SetHoleSize(1000.0); - // Make the triangle winding order consistent + // Make the triangle winding order consistent. vtkNew<vtkPolyDataNormals> normals; normals->SetInputConnection(fillHoles->GetOutputPort()); normals->ConsistencyOn(); @@ -47,11 +47,11 @@ int main(int argc, char* argv[]) normals->GetOutput()->GetPointData()->SetNormals( reader->GetOutput()->GetPointData()->GetNormals()); - // How many added cells + // Determine the number of added cells. vtkIdType numOriginalCells = reader->GetOutput()->GetNumberOfCells(); vtkIdType numNewCells = normals->GetOutput()->GetNumberOfCells(); - // Iterate over the original cells + // Iterate over the original cells. auto it = normals->GetOutput()->NewCellIterator(); vtkIdType numCells = 0; for (it->InitTraversal(); @@ -68,7 +68,7 @@ int main(int argc, char* argv[]) vtkNew<vtkGenericCell> cell; - // The remaining cells are the new ones from the hole filler + // The remaining cells are the new ones from the hole filler. for (; !it->IsDoneWithTraversal(); it->GoToNextCell()) { it->GetCell(cell); @@ -76,7 +76,7 @@ int main(int argc, char* argv[]) } it->Delete(); - // We have to use ConnectivtyFilter and not + // We have to use ConnectivityFilter and not // PolyDataConnectivityFilter since the later does not create // RegionIds cell data. vtkNew<vtkConnectivityFilter> connectivity; @@ -89,7 +89,7 @@ int main(int argc, char* argv[]) // Visualize - // Create a mapper and actor for the fill polydata + // Create a mapper and actor for the fill polydata. vtkNew<vtkDataSetMapper> filledMapper; filledMapper->SetInputConnection(connectivity->GetOutputPort()); filledMapper->SetScalarModeToUseCellData(); @@ -102,7 +102,7 @@ int main(int argc, char* argv[]) filledActor->GetProperty()->SetDiffuseColor( colors->GetColor3d("Peacock").GetData()); - // Create a mapper and actor for the original polydata + // Create a mapper and actor for the original polydata. vtkNew<vtkPolyDataMapper> originalMapper; originalMapper->SetInputConnection(reader->GetOutputPort()); @@ -116,7 +116,7 @@ int main(int argc, char* argv[]) colors->GetColor3d("Flesh").GetData()); originalActor->GetProperty()->SetRepresentationToWireframe(); - // Create a renderer, render window, and interactor + // Create a renderer, render window, and interactor. vtkNew<vtkRenderer> renderer; vtkNew<vtkRenderWindow> renderWindow; @@ -127,7 +127,7 @@ int main(int argc, char* argv[]) vtkNew<vtkRenderWindowInteractor> renderWindowInteractor; renderWindowInteractor->SetRenderWindow(renderWindow); - // Add the actor to the scene + // Add the actors to the scene. renderer->AddActor(originalActor); renderer->AddActor(filledActor); renderer->SetBackground(colors->GetColor3d("Burlywood").GetData()); @@ -139,7 +139,7 @@ int main(int argc, char* argv[]) renderer->GetActiveCamera()->Elevation(30); renderer->ResetCamera(); - // Render and interact + // Render and interact. renderWindow->Render(); renderWindowInteractor->Start(); diff --git a/src/Cxx/Meshes/IdentifyHoles.md b/src/Cxx/Meshes/IdentifyHoles.md index a14c850a1ae..9bfd0c9852b 100644 --- a/src/Cxx/Meshes/IdentifyHoles.md +++ b/src/Cxx/Meshes/IdentifyHoles.md @@ -2,7 +2,7 @@ This example fills the holes in a mesh and then extracts the filled holes as seprate regions. -The example proceeds as follow: +The example proceeds as follows: 1. Read the polydata. 2. Fill the holes with vtkFillHolesFilter. diff --git a/src/Cxx/Utilities/FileOutputWindow.md b/src/Cxx/Utilities/FileOutputWindow.md index 8bb37e456b5..8c34aa3b7b5 100644 --- a/src/Cxx/Utilities/FileOutputWindow.md +++ b/src/Cxx/Utilities/FileOutputWindow.md @@ -1,3 +1,3 @@ ### Description -This example shows how to pipe error output to a text file instead of the usual vtk pop-up window. +This example shows how to pipe error output to a text file instead of the usual vtk pop-up window. The output will also be written to the console. diff --git a/src/PythonicAPI.md b/src/PythonicAPI.md index 5555f3cb60f..74c36ab6e9f 100644 --- a/src/PythonicAPI.md +++ b/src/PythonicAPI.md @@ -228,7 +228,7 @@ This Python script, [SelectExamples](../PythonicAPI/Utilities/SelectExamples), w | Example Name | Description | Image | | -------------- | ------------- | ------- | [CompositePolyDataMapper](/PythonicAPI/CompositeData/CompositePolyDataMapper) | -[OverlappingAMR](/PythonicAPI/CompositeData/OverlappingAMR) | Demonstrates how to create and populate VTK's Overlapping AMR Grid type with data. +[OverlappingAMR](/PythonicAPI/CompositeData/OverlappingAMR) | Demonstrates how to create and populate VTK's Overlapping AMR Grid type with data. ### Data Type Conversions @@ -256,7 +256,9 @@ This section includes examples of manipulating meshes. [DeformPointSet](/PythonicAPI/Meshes/DeformPointSet) | Use the vtkDeformPointSet filter to deform a vtkSphereSource with arbitrary polydata. [DelaunayMesh](/PythonicAPI/Modelling/DelaunayMesh) | Two-dimensional Delaunay triangulation of a random set of points. Points and edges are shown highlighted with sphere glyphs and tubes. [DijkstraGraphGeodesicPath](/PythonicAPI/PolyData/DijkstraGraphGeodesicPath) | Find the shortest path between two points on a mesh. +[FillHoles](/PythonicAPI/Meshes/FillHoles) | Close holes in a mesh. [FitToHeightMap](/PythonicAPI/Meshes/FitToHeightMap) | Drape a polydata over an elevation map. +[IdentifyHoles](/PythonicAPI/PythonicAPI/IdentifyHoles) | Close holes in a mesh and identify the holes. [PointInterpolator](/PythonicAPI/Meshes/PointInterpolator) | Plot a scalar field of points onto a PolyData surface. #### Clipping @@ -343,6 +345,7 @@ This section includes ?vtkUnstructuredGrid?. [BoundingBoxIntersection](/PythonicAPI/Utilities/BoundingBoxIntersection) | Box intersection and Inside tests. [CheckVTKVersion](/PythonicAPI/Utilities/CheckVTKVersion) | Check the VTK version and provide alternatives for different VTK versions. [ClassesInLang1NotInLang2](/PythonicAPI/Utilities/ClassesInLang1NotInLang2) | Select VTK classes with corresponding examples in one language but not in another. +[FileOutputWindow](/PythonicAPI/Utilities/FileOutputWindow) | Write errors to a log file instead of the usual vtk pop-up window. [JSONColorMapToLUT](/PythonicAPI/Utilities/JSONColorMapToLUT) | Take a JSON description of a colormap and convert it to a VTK colormap. [ColorMapToLUT](/PythonicAPI/Utilities/ColorMapToLUT) | Use vtkDiscretizableColorTransferFunction to generate a VTK colormap. [DetermineActorType](/PythonicAPI/Utilities/DetermineActorType) | Determine the type of an actor. diff --git a/src/PythonicAPI/Meshes/FillHoles.md b/src/PythonicAPI/Meshes/FillHoles.md new file mode 100644 index 00000000000..2379d7a7e75 --- /dev/null +++ b/src/PythonicAPI/Meshes/FillHoles.md @@ -0,0 +1,6 @@ +### Description + +This filter finds holes in a mesh and closes them. + +!!! seealso + [IdentifyHoles](../IdentifyHoles) fills the holes and then identifies each hole. diff --git a/src/PythonicAPI/Meshes/FillHoles.py b/src/PythonicAPI/Meshes/FillHoles.py new file mode 100644 index 00000000000..3a5c6938f84 --- /dev/null +++ b/src/PythonicAPI/Meshes/FillHoles.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 + +from pathlib import Path + +# noinspection PyUnresolvedReferences +import vtkmodules.vtkInteractionStyle +# noinspection PyUnresolvedReferences +import vtkmodules.vtkRenderingFreeType +# noinspection PyUnresolvedReferences +import vtkmodules.vtkRenderingOpenGL2 +from vtkmodules.util.execution_model import select_ports +from vtkmodules.vtkCommonColor import vtkNamedColors +from vtkmodules.vtkCommonCore import vtkIdTypeArray +from vtkmodules.vtkCommonDataModel import vtkSelectionNode, vtkSelection +from vtkmodules.vtkFiltersCore import vtkPolyDataNormals +from vtkmodules.vtkFiltersExtraction import vtkExtractSelection +from vtkmodules.vtkFiltersGeometry import vtkDataSetSurfaceFilter +from vtkmodules.vtkFiltersModeling import vtkFillHolesFilter +from vtkmodules.vtkFiltersSources import vtkSphereSource +from vtkmodules.vtkIOGeometry import ( + vtkBYUReader, + vtkOBJReader, + vtkSTLReader +) +from vtkmodules.vtkIOLegacy import vtkPolyDataReader +from vtkmodules.vtkIOPLY import vtkPLYReader +from vtkmodules.vtkIOXML import vtkXMLPolyDataReader +from vtkmodules.vtkRenderingCore import ( + vtkActor, + vtkPolyDataMapper, + vtkProperty, + vtkRenderer, + vtkRenderWindow, + vtkRenderWindowInteractor +) + + +def get_program_parameters(): + import argparse + description = 'Extract a surface from vtkPolyData points.' + epilogue = ''' + ''' + parser = argparse.ArgumentParser(description=description, epilog=epilogue, + formatter_class=argparse.RawTextHelpFormatter) + parser.add_argument('file_name', nargs='?', default=None, + help='The polydata source file name,e.g. Torso.vtp.') + args = parser.parse_args() + + return args.file_name + + +def main(): + restore_original_normals = True + + colors = vtkNamedColors() + + file_name = get_program_parameters() + poly_data = None + if file_name: + if Path(file_name).is_file(): + poly_data = read_poly_data(file_name) + else: + print(f'{file_name} not found.') + if file_name is None or poly_data is None: + poly_data = generate_data() + + fill_holes_filter = vtkFillHolesFilter(input_data=poly_data, hole_size=100000.0) + fill_holes_filter.update() + + # Make the triangle winding order consistent + normals = vtkPolyDataNormals(input_data=fill_holes_filter.output, consistency=True, splitting=False) + + if restore_original_normals: + # Restore the original normals. + normals.update().output.point_data.SetNormals(poly_data.point_data.normals) + + # Visualize + # Define viewport ranges. + # (xmin, ymin, xmax, ymax) + left_viewport = [0.0, 0.0, 0.5, 1.0] + right_viewport = [0.5, 0.0, 1.0, 1.0] + + # Create a mapper and actor. + original_mapper = vtkPolyDataMapper(input_data=poly_data) + + backface_prop = vtkProperty() + backface_prop.diffuse_color = colors.GetColor3d('Banana') + + original_actor = vtkActor(mapper=original_mapper) + original_actor.backface_property = backface_prop + original_actor.property.diffuse_color = colors.GetColor3d('NavajoWhite') + + filled_mapper = vtkPolyDataMapper() + normals >> filled_mapper + + filled_actor = vtkActor(mapper=filled_mapper) + filled_actor.property.diffuse_color = colors.GetColor3d('NavajoWhite') + filled_actor.backface_property = backface_prop + + # Create the renderers, render window, and interactor. + left_renderer = vtkRenderer(viewport=left_viewport, background=colors.GetColor3d('SlateGray')) + right_renderer = vtkRenderer(viewport=right_viewport, background=colors.GetColor3d('LightSlateGray')) + + render_window = vtkRenderWindow(size=(600, 300), window_name='FillHoles') + render_window.AddRenderer(left_renderer) + render_window.AddRenderer(right_renderer) + + render_window_interactor = vtkRenderWindowInteractor() + render_window_interactor.render_window = render_window + + # Add the actor to the scene. + left_renderer.AddActor(original_actor) + right_renderer.AddActor(filled_actor) + + left_renderer.active_camera.position = (0, -1, 0) + left_renderer.active_camera.focal_point = (0, 0, 0) + left_renderer.active_camera.view_up = (0, 0, 1) + left_renderer.active_camera.Azimuth(30) + left_renderer.active_camera.Elevation(30) + + left_renderer.ResetCamera() + + # Share the camera. + right_renderer.active_camera = left_renderer.active_camera + + # Render and interact. + render_window.Render() + + render_window_interactor.Start() + + +def read_poly_data(file_name): + if not file_name: + print(f'No file name.') + return None + + valid_suffixes = ['.g', '.obj', '.stl', '.ply', '.vtk', '.vtp'] + path = Path(file_name) + ext = None + if path.suffix: + ext = path.suffix.lower() + if path.suffix not in valid_suffixes: + print(f'No reader for this file suffix: {ext}') + return None + + reader = None + if ext == '.ply': + reader = vtkPLYReader(file_name=file_name) + elif ext == '.vtp': + reader = vtkXMLPolyDataReader(file_name=file_name) + elif ext == '.obj': + reader = vtkOBJReader(file_name=file_name) + elif ext == '.stl': + reader = vtkSTLReader(file_name=file_name) + elif ext == '.vtk': + reader = vtkPolyDataReader(file_name=file_name) + elif ext == '.g': + reader = vtkBYUReader(file_name=file_name) + + if reader: + reader.update() + poly_data = reader.output + return poly_data + else: + return None + + +def generate_data(): + # Create a sphere. + sphere_source = vtkSphereSource() + sphere_source.Update() + + # Remove some cells. + ids = vtkIdTypeArray() + ids.SetNumberOfComponents(1) + + # Set values. + ids.InsertNextValue(2) + ids.InsertNextValue(10) + + selection_node = vtkSelectionNode(field_type=vtkSelectionNode.CELL, content_type=vtkSelectionNode.INDICES, + selection_list=ids) + selection_node.properties.Set(vtkSelectionNode.INVERSE(), 1) # invert the selection + + selection = vtkSelection() + selection.AddNode(selection_node) + + extract_selection = vtkExtractSelection() + sphere_source >> select_ports(0, extract_selection) + extract_selection.SetInputData(1, selection) + extract_selection.update() + + # In selection. + surface_filter = vtkDataSetSurfaceFilter() + extract_selection >> surface_filter + surface_filter.update() + + return surface_filter.output + + +if __name__ == '__main__': + main() diff --git a/src/PythonicAPI/Meshes/IdentifyHoles.md b/src/PythonicAPI/Meshes/IdentifyHoles.md new file mode 100644 index 00000000000..9bfd0c9852b --- /dev/null +++ b/src/PythonicAPI/Meshes/IdentifyHoles.md @@ -0,0 +1,13 @@ +### Description + +This example fills the holes in a mesh and then extracts the filled holes as seprate regions. + +The example proceeds as follows: + +1. Read the polydata. +2. Fill the holes with vtkFillHolesFilter. +3. Create a new polydata that contains the filled holes. To do this we rely on the fact that the fill holes filter stores the original cells first and then adds the new cells that fill the holes. Using vtkCellIterator, we skip the original cells and then continue iterating to obtain the new cells. +4. Use vtkConnectivityFilter on the filled polydata to identify the individual holes. + +!!! note + We have to use vtkConnectivtyFilter and not vtkPolyDataConnectivityFilter since the later does not create RegionIds cell data. diff --git a/src/PythonicAPI/Meshes/IdentifyHoles.py b/src/PythonicAPI/Meshes/IdentifyHoles.py new file mode 100644 index 00000000000..2317148aae7 --- /dev/null +++ b/src/PythonicAPI/Meshes/IdentifyHoles.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python3 + +from dataclasses import dataclass +from pathlib import Path + +# noinspection PyUnresolvedReferences +import vtkmodules.vtkInteractionStyle +# noinspection PyUnresolvedReferences +import vtkmodules.vtkRenderingFreeType +# noinspection PyUnresolvedReferences +import vtkmodules.vtkRenderingOpenGL2 +from vtkmodules.util.execution_model import select_ports +from vtkmodules.vtkCommonColor import vtkNamedColors +from vtkmodules.vtkCommonCore import vtkIdTypeArray +from vtkmodules.vtkCommonDataModel import ( + vtkGenericCell, + vtkPolyData, + vtkSelection, + vtkSelectionNode +) +from vtkmodules.vtkFiltersCore import ( + vtkConnectivityFilter, + vtkPolyDataNormals +) +from vtkmodules.vtkFiltersExtraction import vtkExtractSelection +from vtkmodules.vtkFiltersGeometry import vtkDataSetSurfaceFilter +from vtkmodules.vtkFiltersModeling import vtkFillHolesFilter +from vtkmodules.vtkFiltersSources import vtkSphereSource +from vtkmodules.vtkIOGeometry import ( + vtkBYUReader, + vtkOBJReader, + vtkSTLReader +) +from vtkmodules.vtkIOLegacy import vtkPolyDataReader +from vtkmodules.vtkIOPLY import vtkPLYReader +from vtkmodules.vtkIOXML import vtkXMLPolyDataReader +from vtkmodules.vtkRenderingCore import ( + vtkActor, + vtkDataSetMapper, + vtkPolyDataMapper, + vtkProperty, + vtkRenderer, + vtkRenderWindow, + vtkRenderWindowInteractor +) + + +def get_program_parameters(): + import argparse + description = 'Close holes in a mesh and identify the holes.' + epilogue = ''' + ''' + parser = argparse.ArgumentParser(description=description, epilog=epilogue, + formatter_class=argparse.RawTextHelpFormatter) + parser.add_argument('file_name', nargs='?', default=None, + help='The polydata source file name,e.g. Torso.vtp.') + args = parser.parse_args() + + return args.file_name + + +def main(): + restore_original_normals = True + + colors = vtkNamedColors() + + file_name = get_program_parameters() + poly_data = None + if file_name: + if Path(file_name).is_file(): + poly_data = read_poly_data(file_name) + else: + print(f'{file_name} not found.') + if file_name is None or poly_data is None: + poly_data = generate_data() + + fill_holes_filter = vtkFillHolesFilter(input_data=poly_data, hole_size=1000.0) + fill_holes_filter.update() + + # Make the triangle winding order consistent. + normals = vtkPolyDataNormals(input_data=fill_holes_filter.output, consistency=True, splitting=False) + + if restore_original_normals: + # Restore the original normals. + normals.update().output.point_data.SetNormals(poly_data.point_data.normals) + + # Determine the number of added cells. + num_original_cells = poly_data.number_of_cells + num_new_cells = normals.output.number_of_cells + + # Iterate over the original cells. + it = normals.output.NewCellIterator() + it.InitTraversal() + numCells = 0 + while not it.IsDoneWithTraversal() and numCells < num_original_cells: + it.GoToNextCell() + numCells += 1 + print( + f'Num original: {num_original_cells}, Num new: {num_new_cells}, Num added: {num_new_cells - num_original_cells}') + + hole_poly_data = vtkPolyData(points=normals.output.points) + hole_poly_data.Allocate(normals.output, num_new_cells - num_original_cells) + + # The remaining cells are the new ones from the hole filler. + cell = vtkGenericCell() + while not it.IsDoneWithTraversal(): + it.GetCell(cell) + hole_poly_data.InsertNextCell(it.GetCellType(), cell.GetPointIds()) + it.GoToNextCell() + + # We have to use ConnectivityFilter and not + # PolyDataConnectivityFilter since the latter does not create + # RegionIds cell data. + connectivity = vtkConnectivityFilter(input_data=hole_poly_data, color_regions=True) + connectivity.SetExtractionModeToAllRegions() + connectivity.update() + print(f'Found {connectivity.GetNumberOfExtractedRegions()} holes') + + # Visualize + + # Create a mapper and actor for the fill polydata. + scalar_range = connectivity.output.cell_data.GetArray('RegionId').range + filled_mapper = vtkDataSetMapper(scalar_mode=Mapper.ScalarMode.VTK_SCALAR_MODE_USE_CELL_DATA, + scalar_range=scalar_range) + connectivity >> filled_mapper + filled_actor = vtkActor(mapper=filled_mapper) + filled_actor.property.diffuse_color = colors.GetColor3d('Peacock') + + # Create a mapper and actor for the original polydata. + original_mapper = vtkPolyDataMapper(input_data=poly_data) + + backface_prop = vtkProperty() + backface_prop.diffuse_color = colors.GetColor3d('Banana') + + original_actor = vtkActor(mapper=original_mapper) + original_actor.backface_property = backface_prop + original_actor.property.diffuse_color = colors.GetColor3d('Flesh') + original_actor.property.SetRepresentationToWireframe() + + # Create a renderer, render window, and interactor. + renderer = vtkRenderer(background=colors.GetColor3d('Burlywood')) + render_window = vtkRenderWindow(size=(512, 512), window_name='IdentifyHoles') + render_window.AddRenderer(renderer) + + render_window_interactor = vtkRenderWindowInteractor() + render_window_interactor.render_window = render_window + + # Add the actors to the scene. + renderer.AddActor(original_actor) + renderer.AddActor(filled_actor) + + renderer.active_camera.position = (0, -1, 0) + renderer.active_camera.focal_point = (0, 0, 0) + renderer.active_camera.view_up = (0, 0, 1) + renderer.active_camera.Azimuth(60) + renderer.active_camera.Elevation(30) + + renderer.ResetCamera() + # Render and interact. + render_window.Render() + + render_window_interactor.Start() + + +def read_poly_data(file_name): + if not file_name: + print(f'No file name.') + return None + + valid_suffixes = ['.g', '.obj', '.stl', '.ply', '.vtk', '.vtp'] + path = Path(file_name) + ext = None + if path.suffix: + ext = path.suffix.lower() + if path.suffix not in valid_suffixes: + print(f'No reader for this file suffix: {ext}') + return None + + reader = None + if ext == '.ply': + reader = vtkPLYReader(file_name=file_name) + elif ext == '.vtp': + reader = vtkXMLPolyDataReader(file_name=file_name) + elif ext == '.obj': + reader = vtkOBJReader(file_name=file_name) + elif ext == '.stl': + reader = vtkSTLReader(file_name=file_name) + elif ext == '.vtk': + reader = vtkPolyDataReader(file_name=file_name) + elif ext == '.g': + reader = vtkBYUReader(file_name=file_name) + + if reader: + reader.update() + poly_data = reader.output + return poly_data + else: + return None + + +def generate_data(): + # Create a sphere. + sphere_source = vtkSphereSource() + sphere_source.Update() + + # Remove some cells. + ids = vtkIdTypeArray() + ids.SetNumberOfComponents(1) + + # Set values. + ids.InsertNextValue(2) + ids.InsertNextValue(10) + + selection_node = vtkSelectionNode(field_type=vtkSelectionNode.CELL, content_type=vtkSelectionNode.INDICES, + selection_list=ids) + selection_node.properties.Set(vtkSelectionNode.INVERSE(), 1) # invert the selection + + selection = vtkSelection() + selection.AddNode(selection_node) + + extract_selection = vtkExtractSelection() + sphere_source >> select_ports(0, extract_selection) + extract_selection.SetInputData(1, selection) + extract_selection.update() + + # In selection. + surface_filter = vtkDataSetSurfaceFilter() + extract_selection >> surface_filter + surface_filter.update() + + return surface_filter.output + + +@dataclass(frozen=True) +class Mapper: + @dataclass(frozen=True) + class ColorMode: + VTK_COLOR_MODE_DEFAULT: int = 0 + VTK_COLOR_MODE_MAP_SCALARS: int = 1 + VTK_COLOR_MODE_DIRECT_SCALARS: int = 2 + + @dataclass(frozen=True) + class ResolveCoincidentTopology: + VTK_RESOLVE_OFF: int = 0 + VTK_RESOLVE_POLYGON_OFFSET: int = 1 + VTK_RESOLVE_SHIFT_ZBUFFER: int = 2 + + @dataclass(frozen=True) + class ScalarMode: + VTK_SCALAR_MODE_DEFAULT: int = 0 + VTK_SCALAR_MODE_USE_POINT_DATA: int = 1 + VTK_SCALAR_MODE_USE_CELL_DATA: int = 2 + VTK_SCALAR_MODE_USE_POINT_FIELD_DATA: int = 3 + VTK_SCALAR_MODE_USE_CELL_FIELD_DATA: int = 4 + VTK_SCALAR_MODE_USE_FIELD_DATA: int = 5 + + +if __name__ == '__main__': + main() diff --git a/src/PythonicAPI/Utilities/FileOutputWindow.md b/src/PythonicAPI/Utilities/FileOutputWindow.md new file mode 100644 index 00000000000..8c34aa3b7b5 --- /dev/null +++ b/src/PythonicAPI/Utilities/FileOutputWindow.md @@ -0,0 +1,3 @@ +### Description + +This example shows how to pipe error output to a text file instead of the usual vtk pop-up window. The output will also be written to the console. diff --git a/src/PythonicAPI/Utilities/FileOutputWindow.py b/src/PythonicAPI/Utilities/FileOutputWindow.py new file mode 100644 index 00000000000..ae8b0647356 --- /dev/null +++ b/src/PythonicAPI/Utilities/FileOutputWindow.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 + +from vtkmodules.vtkCommonCore import ( + vtkFileOutputWindow, + vtkOutputWindow +) +from vtkmodules.vtkIOXML import vtkXMLPolyDataReader + + +def main(): + file_output_window = vtkFileOutputWindow(file_name='FileOutputWindow.txt') + + # Note that the SetInstance function is a static member of vtkOutputWindow. + vtkOutputWindow.SetInstance(file_output_window) + + # This causes an error intentionally (file name not specified) - this error + # will be written to the file output.txt + reader = vtkXMLPolyDataReader() + reader.Update() + + +if __name__ == '__main__': + main() diff --git a/src/Testing/Baseline/Cxx/Meshes/TestFillHoles.png b/src/Testing/Baseline/Cxx/Meshes/TestFillHoles.png index 8fc662fd577..903d957dc59 100644 --- a/src/Testing/Baseline/Cxx/Meshes/TestFillHoles.png +++ b/src/Testing/Baseline/Cxx/Meshes/TestFillHoles.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f808c13df431c672e2a7dd0c47c554422039e806f5a33049295a292fa0cd34e1 -size 43478 +oid sha256:3a06c703d72ee4b6e3cd540c3846967e8e4ff32f80def0957dd648004b7c7d66 +size 43474 diff --git a/src/Testing/Baseline/PythonicAPI/Meshes/TestFillHoles.png b/src/Testing/Baseline/PythonicAPI/Meshes/TestFillHoles.png new file mode 100644 index 00000000000..903d957dc59 --- /dev/null +++ b/src/Testing/Baseline/PythonicAPI/Meshes/TestFillHoles.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a06c703d72ee4b6e3cd540c3846967e8e4ff32f80def0957dd648004b7c7d66 +size 43474 diff --git a/src/Testing/Baseline/PythonicAPI/Meshes/TestIdentifyHoles.png b/src/Testing/Baseline/PythonicAPI/Meshes/TestIdentifyHoles.png new file mode 100644 index 00000000000..d71b1cdbc67 --- /dev/null +++ b/src/Testing/Baseline/PythonicAPI/Meshes/TestIdentifyHoles.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:00d60ab84f05d03d21672d29c64756fd0ca18bc23729b3b3c13c151e1266c0e2 +size 130927 -- GitLab