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