From 9b55aa53fab7a73b996e1ccc7a288f0bea4e22a6 Mon Sep 17 00:00:00 2001 From: Andrew Maclean <andrew.amaclean@gmail.com> Date: Sun, 14 Jul 2024 19:45:03 +1000 Subject: [PATCH] Adding AffineWidget --- src/PythonicAPI.md | 1 + .../PolyData/CombineImportedActors.py | 20 +-- src/PythonicAPI/Widgets/AffineWidget.py | 114 ++++++++++++++++++ .../PythonicAPI/Widgets/TestAffineWidget.png | 3 + 4 files changed, 121 insertions(+), 17 deletions(-) create mode 100644 src/PythonicAPI/Widgets/AffineWidget.py create mode 100644 src/Testing/Baseline/PythonicAPI/Widgets/TestAffineWidget.png diff --git a/src/PythonicAPI.md b/src/PythonicAPI.md index 55c5f715132..9f2eaa54ba7 100644 --- a/src/PythonicAPI.md +++ b/src/PythonicAPI.md @@ -488,6 +488,7 @@ See [this tutorial](http://www.vtk.org/Wiki/VTK/Tutorials/3DDataTypes) for a bri | Example Name | Description | Image | | -------------- | ------------- | ------- | +[AffineWidget](/PythonicAPI/Widgets/AffineWidget) | Apply an affine transformation interactively. [BalloonWidget](/PythonicAPI/Widgets/BalloonWidget) | Uses a vtkBalloonWidget to draw labels when the mouse stays above an actor. [BorderWidget](/PythonicAPI/Widgets/BorderWidget) | 2D selection, 2D box. [BoxWidget](/PythonicAPI/Widgets/BoxWidget) | This 3D widget defines a region of interest that is represented by an arbitrarily oriented hexahedron with interior face angles of 90 degrees (orthogonal faces). The object creates 7 handles that can be moused on and manipulated. diff --git a/src/PythonicAPI/PolyData/CombineImportedActors.py b/src/PythonicAPI/PolyData/CombineImportedActors.py index dde7c57e8b5..2fe12bdb8a9 100644 --- a/src/PythonicAPI/PolyData/CombineImportedActors.py +++ b/src/PythonicAPI/PolyData/CombineImportedActors.py @@ -31,23 +31,22 @@ from vtkmodules.vtkRenderingCore import ( def get_program_parameters(): import argparse - description = 'Importing a 3ds file.' + description = 'Combining imported actors.' epilogue = ''' ''' parser = argparse.ArgumentParser(description=description, epilog=epilogue, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument('in_fn', help='iflamingo.3ds.') - parser.add_argument('-o', '--out_fn', default=None, help='Output file name e.g. iflamingo.obj') # Optional additional input file and folder for the OBJ reader. parser.add_argument('-m', '--mtl_fn', default=None, help='Optional OBJ MTL file name e.g. iflamingo.obj') parser.add_argument('-t', '--texture_dir', default=None, help='Optional OBJ texture folder.') args = parser.parse_args() - return args.in_fn, args.out_fn, args.mtl_fn, args.texture_dir + return args.in_fn, args.mtl_fn, args.texture_dir def main(): - ifn, ofn, mtl_fn, texture_dir = get_program_parameters() + ifn, mtl_fn, texture_dir = get_program_parameters() input_suffixes = ('.3ds', '.glb', '.gltf', '.obj', '.wrl') output_suffixes = ('.glb', '.gltf', '.obj', '.wrl', '.x3d') @@ -65,19 +64,6 @@ def main(): print(f'Available input file suffixes are: {sorted_suffixes(input_suffixes)}') return - ofp = None - if ofn: - ofp = Path(ofn) - if not ofp.suffix.lower() in output_suffixes: - print(f'Available output file suffixes are: {sorted_suffixes(output_suffixes)}') - return - if ofp.is_file(): - print(f'Destination must not exist: {ofp}') - return - if ofp.suffix.lower() == '.obj': - # We may need to create the parent folder. - ofp.parent.mkdir(parents=True, exist_ok=True) - mtlp = None if mtl_fn: mtlp = Path(mtl_fn) diff --git a/src/PythonicAPI/Widgets/AffineWidget.py b/src/PythonicAPI/Widgets/AffineWidget.py new file mode 100644 index 00000000000..423b8195e88 --- /dev/null +++ b/src/PythonicAPI/Widgets/AffineWidget.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 + +from dataclasses import dataclass + +# noinspection PyUnresolvedReferences +import vtkmodules.vtkInteractionStyle +# noinspection PyUnresolvedReferences +import vtkmodules.vtkRenderingOpenGL2 +from vtkmodules.vtkCommonColor import vtkNamedColors +from vtkmodules.vtkCommonCore import vtkCallbackCommand +from vtkmodules.vtkCommonTransforms import vtkTransform +from vtkmodules.vtkFiltersCore import vtkAppendPolyData +from vtkmodules.vtkFiltersSources import ( + vtkPlaneSource, + vtkSphereSource +) +from vtkmodules.vtkInteractionWidgets import vtkAffineWidget +from vtkmodules.vtkRenderingCore import ( + vtkActor, + vtkPolyDataMapper, + vtkRenderWindowInteractor, + vtkRenderWindow, + vtkRenderer +) + + +def main(): + colors = vtkNamedColors() + + # Create two spheres: a larger one and a smaller one on top of the larger one + # to show a reference point while rotating. + # Then append the two spheres into one vtkPolyData. + # Create a mapper and actor for the spheres. + sphere_mapper = vtkPolyDataMapper() + ((vtkSphereSource(), vtkSphereSource(radius=0.075, center=(0, 0.5, 0))) >> + vtkAppendPolyData() >> sphere_mapper) + sphere_actor = vtkActor(mapper=sphere_mapper) + sphere_actor.property.color = colors.GetColor3d('White') + + # Create a plane centered over the larger sphere with 4x4 subsections. + plane_source = vtkPlaneSource(x_resolution=4, y_resolution=4, origin=(-1, -1, 0), + point1=(1, -1, 0), point2=(-1, 1, 0)) + # Create a mapper and actor for the plane and show it as a wireframe. + plane_mapper = vtkPolyDataMapper() + plane_source >> plane_mapper + plane_actor = vtkActor(mapper=plane_mapper) + plane_actor.property.representation = Property.Representation.VTK_WIREFRAME + plane_actor.property.color = colors.GetColor3d('Red') + + ren = vtkRenderer(background=colors.GetColor3d('LightSkyBlue'), + background2=colors.GetColor3d('MidnightBlue'), + gradient_background=True) + ren_win = vtkRenderWindow(size=(640, 480), window_name='AffineWidget') + ren_win.AddRenderer(ren) + iren = vtkRenderWindowInteractor() + iren.render_window = ren_win + iren.interactor_style.SetCurrentStyleToTrackballCamera() + + ren.AddActor(sphere_actor) + ren.AddActor(plane_actor) + + ren_win.Render() + + # Create an affine widget to manipulate the actor + # the widget currently only has a 2D representation and therefore applies + # transforms in the X-Y plane only + affine_widget = vtkAffineWidget(interactor=iren) + affine_widget.CreateDefaultRepresentation() + affine_widget.representation.PlaceWidget(sphere_actor.GetBounds()) + + affine_widget.On() + + affine_callback = AffineCallback(sphere_actor, affine_widget.representation) + + affine_widget.AddObserver(vtkCallbackCommand.InteractionEvent, affine_callback) + affine_widget.AddObserver(vtkCallbackCommand.EndInteractionEvent, affine_callback) + + iren.Start() + + +class AffineCallback(vtkCallbackCommand): + def __init__(self, actor, affine_representation): + super().__init__() + + self.actor = actor + self.affine_rep = affine_representation + self.transform = vtkTransform() + + def __call__(self, caller, ev): + self.Execute(self, id, ev) + + def Execute(self, caller, id, event): + self.affine_rep.GetTransform(self.transform) + self.actor.SetUserTransform(self.transform) + + +@dataclass(frozen=True) +class Property: + @dataclass(frozen=True) + class Interpolation: + VTK_FLAT: int = 0 + VTK_GOURAUD: int = 1 + VTK_PHONG: int = 2 + VTK_PBR: int = 3 + + @dataclass(frozen=True) + class Representation: + VTK_POINTS: int = 0 + VTK_WIREFRAME: int = 1 + VTK_SURFACE: int = 2 + + +if __name__ == '__main__': + main() diff --git a/src/Testing/Baseline/PythonicAPI/Widgets/TestAffineWidget.png b/src/Testing/Baseline/PythonicAPI/Widgets/TestAffineWidget.png new file mode 100644 index 00000000000..534a6bd1987 --- /dev/null +++ b/src/Testing/Baseline/PythonicAPI/Widgets/TestAffineWidget.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5d532835a449a548b746c6580f23bd9a25ffe205f48549e42025ff7adc459f64 +size 182618 -- GitLab