From 4b0fcb0c14772afaba7361be7a683e7a2131ef6f Mon Sep 17 00:00:00 2001
From: Andrew Maclean <andrew.amaclean@gmail.com>
Date: Sat, 21 Dec 2024 11:26:43 +1100
Subject: [PATCH] Fixing callbacks and adding Callbacks.md

---
 src/PythonicAPI.md                            |  1 -
 src/PythonicAPI/Animation/AnimateActors.py    |  3 +-
 src/PythonicAPI/Interaction/CallBack.md       |  8 +-
 src/PythonicAPI/Interaction/CallBack.py       | 82 ++++++++++++-------
 .../PolyData/PolygonalSurfacePointPlacer.py   |  7 +-
 src/PythonicAPI/Snippets.md                   |  2 +-
 src/PythonicAPI/Snippets/Callbacks.md         | 69 ++++++++++++++++
 src/PythonicAPI/Snippets/CameraPosition.md    | 37 ++++++---
 src/PythonicAPI/Visualization/MovableAxes.py  |  8 +-
 src/PythonicAPI/Widgets/AffineWidget.py       |  7 +-
 .../Widgets/ImageTracerWidgetInsideContour.py |  7 +-
 11 files changed, 157 insertions(+), 74 deletions(-)
 create mode 100644 src/PythonicAPI/Snippets/Callbacks.md

diff --git a/src/PythonicAPI.md b/src/PythonicAPI.md
index c5a32d53307..1e6ee9dbfa4 100644
--- a/src/PythonicAPI.md
+++ b/src/PythonicAPI.md
@@ -730,7 +730,6 @@ See [this tutorial](http://www.vtk.org/Wiki/VTK/Tutorials/3DDataTypes) for a bri
 
 | Example Name | Description | Image |
 | -------------- | ------------- | ------- |
-
 [AnimateActors](/PythonicAPI/Animation/AnimateActors) | Animate actors.
 [AnimateSphere](/PythonicAPI/Animation/AnimateSphere) | Animate a sphere by opening it to an angle of 90°.
 [Animation](/PythonicAPI/Utilities/Animation) | Move a sphere across a scene.
diff --git a/src/PythonicAPI/Animation/AnimateActors.py b/src/PythonicAPI/Animation/AnimateActors.py
index 629ad595a12..86822f208cd 100644
--- a/src/PythonicAPI/Animation/AnimateActors.py
+++ b/src/PythonicAPI/Animation/AnimateActors.py
@@ -128,9 +128,8 @@ class AnimationCueObserver:
         self.cue.AddObserver('AnimationCueTickEvent', self)
 
 
-class ActorAnimator(vtkCallbackCommand):
+class ActorAnimator:
     def __init__(self, actor, start_position, end_position):
-        super().__init__()
         self.actor = actor
         self.start_position = start_position
         self.end_position = end_position
diff --git a/src/PythonicAPI/Interaction/CallBack.md b/src/PythonicAPI/Interaction/CallBack.md
index 321a0af4fd1..45150bb7743 100644
--- a/src/PythonicAPI/Interaction/CallBack.md
+++ b/src/PythonicAPI/Interaction/CallBack.md
@@ -23,10 +23,10 @@ In the function **PrintCameraOrientation** note how we convert an array to a vec
 
 ##### Python
 
-In Python the approach is even simpler. We simply define a function to use as the callback with this signature: `def MyCallback(obj, ev):`.
-Then, to pass client data to it, we simply do: `MyCallback.myClientData = myClientData`.
-This relies on the fact that Python functions are in fact objects and we are simply adding new attributes such as `myClientData` in this case.
+In Python the approach is even simpler. 
 
-An alternative method is to define a class passsing the needed variables in the `__init__` function and then implement a `_call__` function that does the work.
+We define a function to use as the callback with this signature: `def my_callback(obj, ev):`. Then, to pass client data to it, we simply do: `my_callback.my_client_data = my_client_data`. This relies on the fact that Python functions are in fact objects and we are simply adding new attributes such as `my_client_data` in this case.
+
+An alternative method is to define a class passing the needed variables in the `__init__` function and then implement a `__call__` function that does the work.
 
 Both approaches are demonstrated in the example.
diff --git a/src/PythonicAPI/Interaction/CallBack.py b/src/PythonicAPI/Interaction/CallBack.py
index fd648cdc41f..48d3616b53d 100755
--- a/src/PythonicAPI/Interaction/CallBack.py
+++ b/src/PythonicAPI/Interaction/CallBack.py
@@ -19,6 +19,7 @@ from vtkmodules.vtkRenderingCore import (
     vtkActor,
     vtkCamera,
     vtkPolyDataMapper,
+    vtkProperty,
     vtkRenderWindow,
     vtkRenderWindowInteractor,
     vtkRenderer
@@ -51,48 +52,42 @@ def main():
     iren = vtkRenderWindowInteractor()
     iren.render_window = ren_win
 
-    # Use a cone as a source with the golden ratio for the height. Because we can!
+    # Use a cone as a source with the golden ratio (φ) for the height. Because we can!
+    # If the short side is one then φ = 2 × sin(54°) or φ = 1/2 + √5 / 2
     source = vtkConeSource(center=(0, 0, 0), radius=1, height=1.6180339887498948482, resolution=128)
 
     # Pipeline
     mapper = vtkPolyDataMapper()
     source >> mapper
-    actor = vtkActor(mapper=mapper)
-    actor.property.color = colors.GetColor3d('Peacock')
-    # Lighting
-    actor.property.ambient = 0.6
-    actor.property.diffuse = 0.2
-    actor.property.specular = 1.0
-    actor.property.specular_power = 20.0
+    # Color and surface properties.
+    actor_property = vtkProperty(color=colors.GetColor3d('Peacock'),
+                                 ambient=0.6, diffuse=0.2,
+                                 specular=1.0, specular_power=20.0)
+    actor = vtkActor(mapper=mapper, property=actor_property)
 
     # Get an outline of the data set for context.
+    outline_property = vtkProperty(color=colors.GetColor3d('Black'), line_width=2)
     outline = vtkOutlineFilter()
     outline_mapper = vtkPolyDataMapper()
     source >> outline >> outline_mapper
-    outline_actor = vtkActor(mapper=outline_mapper)
-    outline_actor.property.color = colors.GetColor3d('Black')
-    outline_actor.property.line_width = 2
+    outline_actor = vtkActor(mapper=outline_mapper, property=outline_property)
 
     # Add the actors to the renderer.
     ren.AddActor(actor)
     ren.AddActor(outline_actor)
 
     # Set up a nice camera position.
-    camera = vtkCamera()
-    camera.position = (4.6, -2.0, 3.8)
-    camera.focal_point = (0.0, 0.0, 0.0)
-    camera.clipping_range = (3.2, 10.2)
-    camera.view_up = (0.3, 1.0, 0.13)
+    camera = vtkCamera(position=(4.6, -2.0, 3.8), focal_point=(0.0, 0.0, 0.0),
+                       clipping_range=(3.2, 10.2), view_up=(0.3, 1.0, 0.13))
     ren.active_camera = camera
 
     ren_win.Render()
 
-    rgb = [0.0] * 4
-    colors.GetColor("Carrot", rgb)
-    rgb = tuple(rgb[:3])
+    rgb = tuple(colors.GetColor3d('Carrot'))
     widget = vtkOrientationMarkerWidget(orientation_marker=make_axes_actor(),
                                         interactor=iren, default_renderer=ren,
-                                        outline_color=rgb, viewport=(0.0, 0.0, 0.2, 0.2), zoom=1.5, enabled=True,
+                                        outline_color=rgb, viewport=(0.0, 0.0, 0.2, 0.2),
+                                        zoom=1.5, enabled=True,
                                         interactive=True)
 
     # Set up the callback.
@@ -107,7 +102,6 @@ def main():
         # Or:
         # observer = OrientationObserver(ren.active_camera)
         # iren.AddObserver('EndInteractionEvent', observer)
-
     iren.Initialize()
     iren.Start()
 
@@ -117,14 +111,17 @@ def get_orientation(caller, ev):
     Print out the orientation.
 
     We must do this before we register the callback in the calling function.
-        GetOrientation.cam = ren.active_camera
+    Add the active camera as an attribute.
+        get_orientation.cam = ren.active_camera
 
     :param caller: The caller.
     :param ev: The event.
     :return:
     """
     # Just do this to demonstrate who called callback and the event that triggered it.
-    print(caller.class_name, 'Event Id:', ev)
+    print(f'Caller: {caller.class_name}, Event Id: {ev}')
+    # Verify that we have a camera in this case.
+    print(f'Camera: {get_orientation.cam.class_name}')
     # Now print the camera orientation.
     camera_orientation(get_orientation.cam)
 
@@ -135,19 +132,42 @@ class OrientationObserver:
 
     def __call__(self, caller, ev):
         # Just do this to demonstrate who called callback and the event that triggered it.
-        print(caller.class_name, 'Event Id:', ev)
+        print(f'Caller: {caller.class_name}, Event Id: {ev}')
+        # Verify that we have a camera in this case.
+        print(f'Camera: {self.cam.class_name}')
         # Now print the camera orientation.
         camera_orientation(self.cam)
 
 
 def camera_orientation(cam):
-    flt_fmt = '9.6g'
-    fmt = '{:' + flt_fmt + '}'
-    print(f'{"Position:":>15s},{" ".join(map(fmt.format, cam.position))}')
-    print(f'{"Focal point:":>15s},{" ".join(map(fmt.format, cam.focal_point))}')
-    print(f'{"Clipping range:":>15s},{" ".join(map(fmt.format, cam.clipping_range))}')
-    print(f'{"View up:":>15s},{" ".join(map(fmt.format, cam.view_up))}')
-    print(f'{"Distance:":>15s},{cam.distance:{flt_fmt}}')
+    flt_fmt = '0.6g'
+    # print(caller.class_name, "modified")
+    # Print the interesting stuff.
+    res = list()
+    res.append(f'Camera orientation:')
+    res.append(f'{"position":>15s} = ({fmt_floats(cam.position)})')
+    res.append(f'{"focal_point":>15s} = ({fmt_floats(cam.focal_point)})')
+    res.append(f'{"view_up":>15s} = ({fmt_floats(cam.view_up)})')
+    res.append(f'{"distance":>15s} = {cam.distance:{flt_fmt}}')
+    res.append(f'{"clipping_range":>15s} = ({fmt_floats(cam.clipping_range)})')
+    print('    \n'.join(res))
+    print()
+
+
+def fmt_floats(v, w=0, d=6, pt='g'):
+    """
+    Pretty print a list or tuple of floats.
+
+    :param v: The list or tuple of floats.
+    :param w: Total width of the field.
+    :param d: The number of decimal places.
+    :param pt: The presentation type, 'f', 'g' or 'e'.
+    :return: A string.
+    """
+    pt = pt.lower()
+    if pt not in ['f', 'g', 'e']:
+        pt = 'f'
+    return ', '.join([f'{element:{w}.{d}{pt}}' for element in v])
 
 
 def make_axes_actor():
diff --git a/src/PythonicAPI/PolyData/PolygonalSurfacePointPlacer.py b/src/PythonicAPI/PolyData/PolygonalSurfacePointPlacer.py
index eab4f7306c0..6bceccf4d1d 100755
--- a/src/PythonicAPI/PolyData/PolygonalSurfacePointPlacer.py
+++ b/src/PythonicAPI/PolyData/PolygonalSurfacePointPlacer.py
@@ -66,16 +66,11 @@ def main():
     interactor.Start()
 
 
-class MyCallback(vtkCallbackCommand):
+class MyCallback:
     def __init__(self, representation):
-        super().__init__()
-
         self.representation = representation
 
     def __call__(self, caller, ev):
-        self.Execute(self, id, ev)
-
-    def Execute(self, caller, id, event):
         print('There are ', self.representation.GetNumberOfNodes(), ' nodes.')
 
 
diff --git a/src/PythonicAPI/Snippets.md b/src/PythonicAPI/Snippets.md
index 65e6bae2eed..d69abba4665 100644
--- a/src/PythonicAPI/Snippets.md
+++ b/src/PythonicAPI/Snippets.md
@@ -2,11 +2,11 @@
 
 Snippets are chunks of code that can be cut (*snipped*) and pasted into examples. We want each example to be stand-alone, so we do not keep the snippet code in a library.
 
-
 ### Available snippets
 
 | Snippet | Description | |
 | -------------- | ------------- | ------- |
+[Callbacks](/PythonicAPI/Snippets/Callbacks.md) | Implementing callbacks.
 [CameraPosition](/PythonicAPI/Snippets/CameraPosition.md) | Get the camera position and focal point.
 [CheckVTKVersion](/PythonicAPI/Snippets/CheckVTKVersion.md) | Check the VTK version returning `True` if the requested VTK version is >= the current version.
 [DrawViewportBorder](/PythonicAPI/Snippets/DrawViewportBorder.md) | Draw a border around a renderer's viewport.
diff --git a/src/PythonicAPI/Snippets/Callbacks.md b/src/PythonicAPI/Snippets/Callbacks.md
new file mode 100644
index 00000000000..4be961f92ad
--- /dev/null
+++ b/src/PythonicAPI/Snippets/Callbacks.md
@@ -0,0 +1,69 @@
+### Description
+
+If a function is passed to another function as an argument, it is known as a callback.
+
+We define a function to use as the callback with this signature: `def my_callback(obj, ev):`. Then, to pass client data to it, we simply do: `my_callback.my_client_data = my_client_data`. This relies on the fact that Python functions are in fact objects and we are simply adding new attributes such as `my_client_data` in this case.
+
+An alternative method is to define a class passing the needed variables in the `__init__` function and then implement a `__call__` function that does the work.
+
+The simplest implementation of a callback is [CameraPosition](../CameraPosition) where we do not even pass client data to it.
+
+An implementation passing client data for both a function and a class is [CallBack](../../Interaction/CallBack), the skeleton code of this is outlined here.
+
+To use the snippet, click the *Copy to clipboard* at the upper right of the code blocks.
+
+### Implementation
+
+``` Python
+
+def get_orientation(caller, ev):
+    """
+    Print out the orientation.
+
+    We must do this before we register the callback in the calling function.
+    Add the active camera as an attribute:
+        get_orientation.cam = ren.active_camera
+
+    :param caller: The caller.
+    :param ev: The event.
+    :return:
+    """
+    # Just do this to demonstrate who called callback and the event that triggered it.
+    print(f'Caller: {caller.class_name}, Event Id: {ev}')
+    # Verify that we have a camera in this case.
+    print(f'Camera: {get_orientation.cam.class_name}')
+    # Now print the camera orientation.
+    # camera_orientation(get_orientation.cam)
+
+
+class OrientationObserver:
+    def __init__(self, cam):
+        self.cam = cam
+
+    def __call__(self, caller, ev):
+        # Just do this to demonstrate who called callback and the event that triggered it.
+        print(f'Caller: {caller.class_name}, Event Id: {ev}')
+        # Verify that we have a camera in this case.
+        print(f'Camera: {self.cam.class_name}')
+        # Now print the camera orientation.
+        # camera_orientation(self.cam)
+
+```
+
+### Usage
+
+``` Python
+    # Set up the callback.
+    if use_function_callback:
+        # We are going to output the camera position when the event
+        #   is triggered, so we add the active camera as an attribute.
+        get_orientation.cam = ren.active_camera
+        # Register the callback with the object that is observing.
+        iren.AddObserver('EndInteractionEvent', get_orientation)
+    else:
+        iren.AddObserver('EndInteractionEvent', OrientationObserver(ren.active_camera))
+        # Or:
+        # observer = OrientationObserver(ren.active_camera)
+        # iren.AddObserver('EndInteractionEvent', observer)
+        
+```
diff --git a/src/PythonicAPI/Snippets/CameraPosition.md b/src/PythonicAPI/Snippets/CameraPosition.md
index 48908834e49..35c2f248ab7 100644
--- a/src/PythonicAPI/Snippets/CameraPosition.md
+++ b/src/PythonicAPI/Snippets/CameraPosition.md
@@ -7,7 +7,6 @@ To use the snippet, click the *Copy to clipboard* at the upper right of the code
 ### Implementation
 
 ``` Python
-
 def camera_modified_callback(caller, event):
     """
      Used to estimate positions similar to the book illustrations.
@@ -15,16 +14,34 @@ def camera_modified_callback(caller, event):
     :param event:
     :return:
     """
-    print(caller.class_name, "modified")
+    flt_fmt = '0.6g'
+    # print(caller.class_name, "modified")
     # Print the interesting stuff.
-    res = f'\tcamera = ren.active_camera\n'
-    res += f'\tcamera.position = ({", ".join(map("{0:0.6f}".format, caller.position))})\n'
-    res += f'\tcamera.focal_point = ({", ".join(map("{0:0.6f}".format, caller.focal_point))})\n'
-    res += f'\tcamera.view_up = ({", ".join(map("{0:0.6f}".format, caller.view_up))})\n'
-    res += f'\tcamera.distance = {"{0:0.6f}".format(caller.GetDistance())}\n'
-    res += f'\tcamera.clipping_range = ({", ".join(map("{0:0.6f}".format, caller.clipping_range))})\n'
-    print(res)
+    res = list()
+    res.append(f'    camera = ren.active_camera')
+    res.append(f'    camera.position = ({fmt_floats(caller.position)})')
+    res.append(f'    camera.focal_point = ({fmt_floats(caller.focal_point)})')
+    res.append(f'    camera.view_up = ({fmt_floats(caller.view_up)})')
+    res.append(f'    camera.distance = {caller.distance:{flt_fmt}}')
+    res.append(f'    camera.clipping_range = ({fmt_floats(caller.clipping_range)})')
+    print('    \n'.join(res))
+    print()
+
+
+def fmt_floats(v, w=0, d=6, pt='g'):
+    """
+    Pretty print a list or tuple of floats.
 
+    :param v: The list or tuple of floats.
+    :param w: Total width of the field.
+    :param d: The number of decimal places.
+    :param pt: The presentation type, 'f', 'g' or 'e'.
+    :return: A string.
+    """
+    pt = pt.lower()
+    if pt not in ['f', 'g', 'e']:
+        pt = 'f'
+    return ', '.join([f'{element:{w}.{d}{pt}}' for element in v])
 ```
 
 ### Usage
@@ -34,4 +51,4 @@ def camera_modified_callback(caller, event):
     ren.active_camera.AddObserver('ModifiedEvent', camera_modified_callback)
 ```
 
-Once you have the output, replace the `ren.GetActiveCamera().AddObserver...` line with the output data.
+Once you have the output, replace the `ren.active_camera.AddObserver(...)` line with the output data.
diff --git a/src/PythonicAPI/Visualization/MovableAxes.py b/src/PythonicAPI/Visualization/MovableAxes.py
index 9dcad6abe97..c56076cee5b 100755
--- a/src/PythonicAPI/Visualization/MovableAxes.py
+++ b/src/PythonicAPI/Visualization/MovableAxes.py
@@ -3,7 +3,6 @@ import vtkmodules.vtkInteractionStyle
 # noinspection PyUnresolvedReferences
 import vtkmodules.vtkRenderingOpenGL2
 from vtkmodules.vtkCommonColor import vtkNamedColors
-from vtkmodules.vtkCommonCore import vtkCallbackCommand
 from vtkmodules.vtkFiltersSources import (
     vtkConeSource
 )
@@ -102,10 +101,8 @@ def main():
     render_window_interactor.Start()
 
 
-class PositionCallback(vtkCallbackCommand):
+class PositionCallback:
     def __init__(self, x_label, y_label, z_label, axes):
-        super().__init__()
-
         self.x_label = x_label
         self.y_label = y_label
         self.z_label = z_label
@@ -113,9 +110,6 @@ class PositionCallback(vtkCallbackCommand):
         self.followers = [self.x_label, self.y_label, self.z_label]
 
     def __call__(self, caller, ev):
-        self.Execute(caller, ev)
-
-    def Execute(self, caller, eid, call_data=None):
         self.axes.InitPathTraversal()
         count = 0
 
diff --git a/src/PythonicAPI/Widgets/AffineWidget.py b/src/PythonicAPI/Widgets/AffineWidget.py
index 423b8195e88..1a1a5d5a81a 100755
--- a/src/PythonicAPI/Widgets/AffineWidget.py
+++ b/src/PythonicAPI/Widgets/AffineWidget.py
@@ -78,18 +78,13 @@ def main():
     iren.Start()
 
 
-class AffineCallback(vtkCallbackCommand):
+class AffineCallback:
     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)
 
diff --git a/src/PythonicAPI/Widgets/ImageTracerWidgetInsideContour.py b/src/PythonicAPI/Widgets/ImageTracerWidgetInsideContour.py
index de595390fed..23467643e59 100755
--- a/src/PythonicAPI/Widgets/ImageTracerWidgetInsideContour.py
+++ b/src/PythonicAPI/Widgets/ImageTracerWidgetInsideContour.py
@@ -57,17 +57,12 @@ def main():
     interactor.Start()
 
 
-class AffineCallback(vtkCallbackCommand):
+class AffineCallback:
     def __init__(self, image, tracer_widget):
-        super().__init__()
-
         self.image = image
         self.tracer_widget = tracer_widget
 
     def __call__(self, caller, ev):
-        self.Execute(self, id, ev)
-
-    def Execute(self, caller, identifier, event):
         path = vtkPolyData()
 
         if not self.tracer_widget.IsClosed():
-- 
GitLab