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