vedo.interactor_modes

Submodule to customize interaction modes.

   1#!/usr/bin/env python
   2# -*- coding: utf-8 -*-
   3from dataclasses import dataclass
   4from typing import Optional
   5import numpy as np
   6
   7import vedo.vtkclasses as vtki
   8
   9__docformat__ = "google"
  10
  11__doc__ = """Submodule to customize interaction modes."""
  12
  13
  14class MousePan(vtki.vtkInteractorStyleUser):
  15    """
  16    Interaction mode to pan the scene by dragging the mouse.
  17
  18    Controls:
  19    - Left mouse button will pan the scene.
  20    - Mouse middle button up/down is elevation, and left and right is azimuth.
  21    - Right mouse button is rotate (left/right movement) and zoom in/out (up/down movement)
  22    - Mouse scroll wheel is zoom in/out
  23
  24    Arguments:
  25        enable_pan: (bool)
  26            Enable panning the scene.
  27        enable_zoom: (bool)
  28            Enable zooming the scene.
  29        enable_rotate: (bool)
  30            Enable rotating the scene.
  31        speed: (float)
  32            Speed factor of the interaction, e.i. how fast the scene must move.
  33    """
  34
  35    def __init__(self, enable_pan=True, enable_zoom=True, enable_rotate=True, speed=1.0):
  36
  37        super().__init__()
  38
  39        self.left = False
  40        self.middle = False
  41        self.right = False
  42        self.speed = speed
  43
  44        self.interactor = None
  45        self.renderer = None
  46        self.camera = None
  47
  48        self.oldpickD = []
  49        self.newpickD = []
  50        self.oldpickW = np.array([0, 0, 0, 0], dtype=float)
  51        self.newpickW = np.array([0, 0, 0, 0], dtype=float)
  52        self.fpD = np.array([0, 0, 0], dtype=float)
  53        self.fpW = np.array([0, 0, 0], dtype=float)
  54        self.posW = np.array([0, 0, 0], dtype=float)
  55        self.motionD = np.array([0, 0], dtype=float)
  56        self.motionW = np.array([0, 0, 0], dtype=float)
  57
  58        # print("MousePan: Left mouse button to pan the scene.")
  59        self.AddObserver("LeftButtonPressEvent", self._left_down)
  60        self.AddObserver("LeftButtonReleaseEvent", self._left_up)
  61
  62        if enable_rotate:
  63            self.AddObserver("MiddleButtonPressEvent", self._middle_down)
  64            self.AddObserver("MiddleButtonReleaseEvent", self._middle_up)
  65
  66        if enable_zoom:
  67            self.AddObserver("RightButtonPressEvent", self._right_down)
  68            self.AddObserver("RightButtonReleaseEvent", self._right_up)
  69            self.AddObserver("MouseWheelForwardEvent", self._wheel_forward)
  70            self.AddObserver("MouseWheelBackwardEvent", self._wheel_backward)
  71
  72        if enable_pan:
  73            self.AddObserver("MouseMoveEvent", self._mouse_move)
  74
  75    def _get_motion(self):
  76        self.oldpickD = np.array(self.interactor.GetLastEventPosition())
  77        self.newpickD = np.array(self.interactor.GetEventPosition())
  78        self.motionD = (self.newpickD - self.oldpickD) / 4
  79        self.camera = self.renderer.GetActiveCamera()
  80        self.fpW = self.camera.GetFocalPoint()
  81        self.posW = self.camera.GetPosition()
  82        self.ComputeWorldToDisplay(self.renderer, self.fpW[0], self.fpW[1], self.fpW[2], self.fpD)
  83        focaldepth = self.fpD[2]
  84        self.ComputeDisplayToWorld(
  85            self.renderer, self.oldpickD[0], self.oldpickD[1], focaldepth, self.oldpickW
  86        )
  87        self.ComputeDisplayToWorld(
  88            self.renderer, self.newpickD[0], self.newpickD[1], focaldepth, self.newpickW
  89        )
  90        self.motionW[:3] = (self.oldpickW[:3] - self.newpickW[:3]) * self.speed
  91
  92    def _mouse_left_move(self):
  93        self._get_motion()
  94        self.camera.SetFocalPoint(self.fpW[:3] + self.motionW[:3])
  95        self.camera.SetPosition(self.posW[:3] + self.motionW[:3])
  96        self.interactor.Render()
  97
  98    def _mouse_middle_move(self):
  99        self._get_motion()
 100        if abs(self.motionD[0]) > abs(self.motionD[1]):
 101            self.camera.Azimuth(-2 * self.speed * self.motionD[0])
 102        else:
 103            self.camera.Elevation(-self.speed *self.motionD[1])
 104        self.interactor.Render()
 105
 106    def _mouse_right_move(self):
 107        self._get_motion()
 108        if abs(self.motionD[0]) > abs(self.motionD[1]):
 109            self.camera.Azimuth(-2.0 * self.speed * self.motionD[0])
 110        else:
 111            self.camera.Zoom(1 + self.motionD[1] / 100)
 112        self.interactor.Render()
 113
 114    def _mouse_wheel_forward(self):
 115        self.camera = self.renderer.GetActiveCamera()
 116        self.camera.Zoom(1.1 * self.speed)
 117        self.interactor.Render()
 118
 119    def _mouse_wheel_backward(self):
 120        self.camera = self.renderer.GetActiveCamera()
 121        self.camera.Zoom(0.9 * self.speed)
 122        self.interactor.Render()
 123
 124    def _left_down(self, _w, _e):
 125        self.left = True
 126
 127    def _left_up(self, _w, _e):
 128        self.left = False
 129
 130    def _middle_down(self, _w, _e):
 131        self.middle = True
 132
 133    def _middle_up(self, _w, _e):
 134        self.middle = False
 135
 136    def _right_down(self, _w, _e):
 137        self.right = True
 138
 139    def _right_up(self, _w, _e):
 140        self.right = False
 141
 142    def _wheel_forward(self, _w, _e):
 143        self._mouse_wheel_forward()
 144
 145    def _wheel_backward(self, _w, _e):
 146        self._mouse_wheel_backward()
 147
 148    def _mouse_move(self, _w, _e):
 149        if self.left:
 150            self._mouse_left_move()
 151        if self.middle:
 152            self._mouse_middle_move()
 153        if self.right:
 154            self._mouse_right_move()
 155
 156
 157#############################################################################
 158class FlyOverSurface(vtki.vtkInteractorStyleUser):
 159    """
 160    Interaction mode to fly over a surface.
 161
 162    Controls:
 163        - Press arrows to move the camera in the plane of the surface.
 164        - "t" (or "PageUp") will move the camera upper in z.
 165        - "g" (or "PageDown") will move the camera lower in z.
 166        - "x" and "X" will reset the camera to the default position towards positive or negative x.
 167        - "y" and "Y" will reset the camera to the default position towards positive or negative y.
 168        - "." and "," will rotate azimuth to the right or left.
 169        - "r" will reset the camera to the default position.
 170
 171    Left button: Select a point on the surface to focus the camera on it.
 172    """
 173
 174    def __init__(self, move_step=0.05, angle_step=1.5):
 175        """
 176        Interaction mode to fly over a surface.
 177
 178        Arguments:
 179            move_step: float, optional
 180                The step size for moving the camera in the plane of the surface.
 181            angle_step: float, optional
 182                The step size for rotating the camera.
 183
 184        Example:
 185            [interaction_modes3.py](https://github.com/marcomusy/vedo/blob/master/examples/basic/interaction_modes3.py)
 186        """
 187
 188        super().__init__()
 189
 190        self.interactor = None  # filled in plotter.py
 191        self.renderer = None  # filled in plotter.py
 192
 193        self.angle_step = angle_step
 194        self.move_step = move_step
 195
 196        self.tleft = vtki.vtkTransform()
 197        self.tleft.RotateZ(self.angle_step)
 198
 199        self.tright = vtki.vtkTransform()
 200        self.tright.RotateZ(-self.angle_step)
 201
 202        self.bounds = ()
 203
 204        self.AddObserver("KeyPressEvent", self._key)
 205        self.AddObserver("RightButtonPressEvent", self._right_button_press)
 206        self.AddObserver("MouseWheelForwardEvent", self._mouse_wheel_forward)
 207        self.AddObserver("MouseWheelBackwardEvent", self._mouse_wheel_backward)
 208        self.AddObserver("LeftButtonPressEvent", self._left_button_press)
 209
 210    @property
 211    def camera(self):
 212        """Return the active camera."""
 213        return self.renderer.GetActiveCamera()
 214
 215    @property
 216    def position(self):
 217        """Return the camera position."""
 218        return np.array(self.camera.GetPosition())
 219
 220    @position.setter
 221    def position(self, value):
 222        """Set the camera position."""
 223        self.camera.SetPosition(value[:3])
 224        self.camera.SetViewUp(0.00001, 0, 1)
 225        self.renderer.ResetCameraClippingRange()
 226
 227    @property
 228    def focal_point(self):
 229        """Return the camera focal point."""
 230        return np.array(self.camera.GetFocalPoint())
 231
 232    @focal_point.setter
 233    def focal_point(self, value):
 234        """Set the camera focal point."""
 235        self.camera.SetFocalPoint(value[:3])
 236        self.camera.SetViewUp(0.00001, 0, 1)
 237        self.renderer.ResetCameraClippingRange()
 238
 239    def _right_button_press(self, _obj, event):
 240        """Right button press."""
 241        # print("Right button", event)
 242        self._key("Down", event)
 243
 244    def _left_button_press(self, obj, _event):
 245        """Left button press."""
 246        # print("Left button", event)
 247        newPickPoint = [0, 0, 0, 0]
 248        focalDepth = 0
 249        x, y = obj.interactor.GetEventPosition()
 250        self.ComputeDisplayToWorld(self.renderer, x, y, focalDepth, newPickPoint)
 251        self.focal_point = np.array(newPickPoint)
 252        self.interactor.Render()
 253
 254    def _mouse_wheel_backward(self, _obj, event):
 255        """Mouse wheel backward."""
 256        # print("mouse_wheel_backward ", event)
 257        self._key("Down", event)
 258
 259    def _mouse_wheel_forward(self, _obj, event):
 260        """Mouse wheel forward."""
 261        # print("mouse_wheel_forward ", event)
 262        self._key("Up", event)
 263
 264    def _key(self, obj, event):
 265        """Key press."""
 266        if "Mouse" in event or "Button" in event:
 267            k = obj
 268        else:
 269            k = obj.GetKeySym()
 270            if obj.GetShiftKey():
 271                k = k.upper()
 272        # print("FlyOverSurface. Key press", k)
 273
 274        if k in ["r", "x"]:
 275            # print("r pressed, reset camera")
 276            self.bounds = self.renderer.ComputeVisiblePropBounds()
 277            x0, x1, y0, y1, z0, z1 = self.bounds
 278            dx = x1 - x0
 279            z = max(z1 * 1, z0 + (y1 - y0) / 4, z0 + (x1 - x0) / 4)
 280            self.position = [x0 - dx, (y0 + y1) / 2, z]
 281            self.focal_point = [x0 + dx / 2, (y0 + y1) / 2, z]
 282        elif k in ["X"]:
 283            # print("X pressed, reset camera")
 284            self.bounds = self.renderer.ComputeVisiblePropBounds()
 285            x0, x1, y0, y1, z0, z1 = self.bounds
 286            dx = x1 - x0
 287            z = max(z1 * 1, (y1 - y0) / 4, (x1 - x0) / 4)
 288            self.position = [x1 + dx, (y0 + y1) / 2, z]
 289            self.focal_point = [x0 - dx / 2, (y0 + y1) / 2, z]
 290
 291        elif k in ["y"]:
 292            # print("y pressed, reset camera")
 293            self.bounds = self.renderer.ComputeVisiblePropBounds()
 294            x0, x1, y0, y1, z0, z1 = self.bounds
 295            dy = y1 - y0
 296            z = max(z1 * 1, z0 + (y1 - y0) / 4, z0 + (x1 - x0) / 4)
 297            self.position = [(x0 + x1) / 2, y0 - dy, z]
 298            self.focal_point = [(x0 + x1) / 2, y1 + dy / 2, z]
 299        elif k in ["Y"]:
 300            # print("Y pressed, reset camera")
 301            self.bounds = self.renderer.ComputeVisiblePropBounds()
 302            x0, x1, y0, y1, z0, z1 = self.bounds
 303            dy = y1 - y0
 304            z = max(z1 * 1, z0 + (y1 - y0) / 4, z0 + (x1 - x0) / 4)
 305            self.position = [(x0 + x1) / 2, y1 + dy, z]
 306            self.focal_point = [(x0 + x1) / 2, y0 - dy / 2, z]
 307
 308        elif k in ["Up", "w"]:
 309            # print("Up pressed, move forward")
 310            self.bounds = self.renderer.ComputeVisiblePropBounds()
 311            diagonal = np.linalg.norm(np.array(self.bounds[1::2]) - np.array(self.bounds[::2]))
 312            dx = self.move_step * diagonal
 313            p = np.array(self.camera.GetPosition())
 314            v = np.array(self.camera.GetDirectionOfProjection())
 315            newp = p + dx * v
 316            self.position = [newp[0], newp[1], p[2]]
 317            self.focal_point = self.focal_point + dx * v
 318        elif k in ["Down", "s"]:
 319            # print("Down pressed, move backward")
 320            self.bounds = self.renderer.ComputeVisiblePropBounds()
 321            diagonal = np.linalg.norm(np.array(self.bounds[1::2]) - np.array(self.bounds[::2]))
 322            dx = self.move_step * diagonal
 323            p = np.array(self.camera.GetPosition())
 324            v = np.array(self.camera.GetDirectionOfProjection())
 325            newp = p - dx * v
 326            self.position = [newp[0], newp[1], p[2]]
 327            self.focal_point = self.focal_point - dx * v
 328
 329        elif k in ["Left", "a"]:
 330            # print("Left pressed, rotate to the left")
 331            self.bounds = self.renderer.ComputeVisiblePropBounds()
 332            diagonal = np.linalg.norm(np.array(self.bounds[1::2]) - np.array(self.bounds[::2]))
 333            w = np.array(self.camera.GetDirectionOfProjection())
 334            p = np.array(self.camera.GetPosition())
 335            w2 = np.array(self.tleft.TransformFloatPoint(w))
 336            self.focal_point = self.focal_point + np.linalg.norm(p-self.focal_point) * w2
 337
 338        elif k in ["Right", "d"]:
 339            # print("Right pressed, rotate to the right")
 340            self.bounds = self.renderer.ComputeVisiblePropBounds()
 341            diagonal = np.linalg.norm(np.array(self.bounds[1::2]) - np.array(self.bounds[::2]))
 342            w = np.array(self.camera.GetDirectionOfProjection())
 343            p = np.array(self.camera.GetPosition())
 344            w2 = np.array(self.tright.TransformFloatPoint(w))
 345            self.focal_point = self.focal_point + np.linalg.norm(p-self.focal_point) * w2
 346
 347        elif k in ["t", "Prior"]:
 348            # print("t pressed, move z up")
 349            self.bounds = self.renderer.ComputeVisiblePropBounds()
 350            diagonal = np.linalg.norm(np.array(self.bounds[1::2]) - np.array(self.bounds[::2]))
 351            dx = self.move_step * diagonal
 352            p = self.position
 353            self.position = [p[0], p[1], p[2] + dx / 4]
 354
 355        elif k in ["g", "Next"]:
 356            # print("g pressed, move z down")
 357            self.bounds = self.renderer.ComputeVisiblePropBounds()
 358            diagonal = np.linalg.norm(np.array(self.bounds[1::2]) - np.array(self.bounds[::2]))
 359            dx = self.move_step * diagonal
 360            p = self.position
 361            self.position = [p[0], p[1], p[2] - dx / 4]
 362
 363        elif k in ["comma", "COMMA"]:
 364            # print("< pressed, rotate azimuth to the left")
 365            self.bounds = self.renderer.ComputeVisiblePropBounds()
 366            scene_center = [
 367                self.bounds[0] + self.bounds[1],
 368                self.bounds[2] + self.bounds[3],
 369                self.bounds[4] + self.bounds[5],
 370            ]
 371            scene_center = np.array(scene_center) / 2
 372            p = self.position
 373            v = p - scene_center
 374            newp = scene_center + self.tleft.TransformFloatPoint(v)
 375            self.position = [newp[0], newp[1], p[2]]
 376
 377        elif k in ["period", "PERIOD"]:
 378            # print("< pressed, rotate azimuth to the right")
 379            self.bounds = self.renderer.ComputeVisiblePropBounds()
 380            scene_center = [
 381                self.bounds[0] + self.bounds[1],
 382                self.bounds[2] + self.bounds[3],
 383                self.bounds[4] + self.bounds[5],
 384            ]
 385            scene_center = np.array(scene_center) / 2
 386            p = self.position
 387            v = p - scene_center
 388            newp = scene_center + self.tright.TransformFloatPoint(v)
 389            self.position = [newp[0], newp[1], p[2]]
 390
 391        elif k in ["q", "Return"]:
 392            self.interactor.ExitCallback()
 393            return
 394
 395        else:
 396            return
 397
 398        self.interactor.Render()
 399
 400
 401###################################################################################
 402@dataclass
 403class _BlenderStyleDragInfo:
 404    """Data structure containing the data required to execute dragging a node"""
 405
 406    # Scene related
 407    dragged_node = None  # Node
 408
 409    # VTK related
 410    actors_dragging: list
 411    dragged_actors_original_positions: list  # original VTK positions
 412    start_position_3d = np.array((0, 0, 0))  # start position of the cursor
 413
 414    delta = np.array((0, 0, 0))
 415
 416    def __init__(self):
 417        self.actors_dragging = []
 418        self.dragged_actors_original_positions = []
 419
 420
 421###############################################
 422class BlenderStyle(vtki.vtkInteractorStyleUser):
 423    """
 424    Create an interaction style using the Blender default key-bindings.
 425
 426    Camera action code is largely a translation of
 427    [this](https://github.com/Kitware/VTK/blob/master/Interaction/Style/vtkInteractorStyleTrackballCamera.cxx)
 428    Rubber band code
 429    [here](https://gitlab.kitware.com/updega2/vtk/-/blob/d324b2e898b0da080edee76159c2f92e6f71abe2/Rendering/vtkInteractorStyleRubberBandZoom.cxx)
 430
 431    Interaction:
 432        - Left button: select
 433        - Left button drag: rubber band select or line select, depends on the dragged distance
 434
 435        - Middle button         : rotate
 436        - Middle button + shift : pan
 437        - Middle button + ctrl  : zoom
 438        - Middle button + alt   : center view on picked point
 439        - Middle button + alt   : zoom rubber band
 440
 441        - Mouse wheel : zoom
 442
 443        - Right key click: reserved for context-menu
 444
 445
 446    Keys:
 447        - 2 or 3 : toggle perspective view
 448        - a      : zoom all
 449        - x,y,z  : view direction (toggles positive and negative)
 450        - left/right arrows: rotate 45 deg clockwise/ccw about z-axis, snaps to nearest 45 deg
 451        - b      : box zoom
 452        - m      : mouse middle lock (toggles)
 453        - space  : same as middle mouse button
 454        - g      : grab (move actors)
 455        - enter  : accept drag
 456        - esc    : cancel drag, call callbackEscape
 457
 458
 459    Laptop Mode
 460    -----------
 461    Use space or `m` as replacement for middle button (`m` is sticky, space is not),
 462    if `callback_any_key` is assigned then this function is called on every key press.
 463    If this function returns True then further processing of events is stopped.
 464
 465
 466    Moving Actors
 467    -------------
 468    Actors can be moved interactively by the user.
 469    To support custom groups of actors to be moved as a whole the following system
 470    is implemented:
 471
 472    When 'g' is pressed (grab) then a list of all the actors that are being dragged.
 473    By default this is the selection, but this may be altered.
 474    Drag is accepted using enter, click, or g. Drag is cancelled by esc
 475
 476    Events
 477    ------
 478    - `callback_start_drag` is called when initializing the drag.
 479    This is when to assign actors and other data to draginfo.
 480    - `callback_end_drag` is called when the drag is accepted.
 481
 482    Responding to other events
 483    --------------------------
 484    `callback_camera_direction_changed` : executed when camera has rotated but before re-rendering
 485
 486    Example:
 487        [interaction_modes2.py](https://github.com/marcomusy/vedo/blob/master/examples/basic/interaction_modes2.py)
 488
 489    .. note::
 490        This class is based on R. de Bruin's
 491        [DAVE](https://github.com/RubendeBruin/DAVE/blob/master/src/DAVE/visual_helpers/vtkBlenderLikeInteractionStyle.py)
 492        implementation as discussed [in this issue](https://github.com/marcomusy/vedo/discussions/788).
 493    """
 494
 495    def __init__(self):
 496
 497        super().__init__()
 498
 499        self.interactor = None
 500        self.renderer = None
 501
 502        # callback_select is called whenever one or mode props are selected.
 503        # callback will be called with a list of props of which the first entry
 504        # is prop closest to the camera.
 505        self.callback_select = None
 506        self.callback_start_drag = None
 507        self.callback_end_drag = None
 508        self.callback_escape_key = None
 509        self.callback_delete_key = None
 510        self.callback_focus_key = None
 511        self.callback_any_key = None
 512        self.callback_measure = None  # callback with argument float (meters)
 513        self.callback_camera_direction_changed = None
 514
 515        # active drag
 516        # assigned to a _BlenderStyleDragInfo object when dragging is active
 517        self.draginfo: Optional[_BlenderStyleDragInfo] = None
 518
 519        # picking
 520        self.picked_props = []  # will be filled by latest pick
 521
 522        # settings
 523        self.mouse_motion_factor = 20
 524        self.mouse_wheel_motion_factor = 0.1
 525        self.zoom_motion_factor = 0.25
 526
 527        # internals
 528        self.start_x = 0  # start of a drag
 529        self.start_y = 0
 530        self.end_x = 0
 531        self.end_y = 0
 532
 533        self.middle_mouse_lock = False
 534        self.middle_mouse_lock_actor = None  # will be created when required
 535
 536        # Special Modes
 537        self._is_box_zooming = False
 538
 539        # holds an image of the renderer output at the start of a drawing event
 540        self._pixel_array = vtki.vtkUnsignedCharArray()
 541
 542        self._upside_down = False
 543
 544        self._left_button_down = False
 545        self._middle_button_down = False
 546
 547        self.AddObserver("RightButtonPressEvent", self.right_button_press)
 548        self.AddObserver("RightButtonReleaseEvent", self.right_button_release)
 549        self.AddObserver("MiddleButtonPressEvent", self.middle_button_press)
 550        self.AddObserver("MiddleButtonReleaseEvent", self.middle_button_release)
 551        self.AddObserver("MouseWheelForwardEvent", self.mouse_wheel_forward)
 552        self.AddObserver("MouseWheelBackwardEvent", self.mouse_wheel_backward)
 553        self.AddObserver("LeftButtonPressEvent", self.left_button_press)
 554        self.AddObserver("LeftButtonReleaseEvent", self.left_button_release)
 555        self.AddObserver("MouseMoveEvent", self.mouse_move)
 556        self.AddObserver("WindowResizeEvent", self.window_resized)
 557        # ^does not seem to fire!
 558        self.AddObserver("KeyPressEvent", self.key_press)
 559        self.AddObserver("KeyReleaseEvent", self.key_release)
 560
 561    def right_button_press(self, _obj, _event):
 562        pass
 563
 564    def right_button_release(self, _obj, _event):
 565        pass
 566
 567    def middle_button_press(self, _obj, _event):
 568        self._middle_button_down = True
 569
 570    def middle_button_release(self, _obj, _event):
 571        self._middle_button_down = False
 572
 573        # perform middle button focus event if ALT is down
 574        if self.GetInteractor().GetAltKey():
 575            # print("Middle button released while ALT is down")
 576            # try to pick an object at the current mouse position
 577            rwi = self.GetInteractor()
 578            self.start_x, self.start_y = rwi.GetEventPosition()
 579            props = self.perform_picking_on_selection()
 580
 581            if props:
 582                self.focus_on(props[0])
 583
 584    def mouse_wheel_backward(self, _obj, _event):
 585        self.move_mouse_wheel(-1)
 586
 587    def mouse_wheel_forward(self, _obj, _event):
 588        self.move_mouse_wheel(1)
 589
 590    def mouse_move(self, _obj, _event):
 591        interactor = self.GetInteractor()
 592
 593        # Find the renderer that is active below the current mouse position
 594        x, y = interactor.GetEventPosition()
 595        self.FindPokedRenderer(x, y)
 596
 597        Shift = interactor.GetShiftKey()
 598        Ctrl = interactor.GetControlKey()
 599        Alt = interactor.GetAltKey()
 600
 601        MiddleButton = self._middle_button_down or self.middle_mouse_lock
 602
 603        # start with the special modes
 604        if self._is_box_zooming:
 605            self.draw_dragged_selection()
 606        elif MiddleButton and not Shift and not Ctrl and not Alt:
 607            self.rotate()
 608        elif MiddleButton and Shift and not Ctrl and not Alt:
 609            self.pan()
 610        elif MiddleButton and Ctrl and not Shift and not Alt:
 611            self.zoom()  # Dolly
 612        elif self.draginfo is not None:
 613            self.execute_drag()
 614        elif self._left_button_down and Ctrl and Shift:
 615            self.draw_measurement()
 616        elif self._left_button_down:
 617            self.draw_dragged_selection()
 618
 619        self.InvokeEvent("InteractionEvent", None)
 620
 621    def move_mouse_wheel(self, direction):
 622        rwi = self.GetInteractor()
 623
 624        # Find the renderer that is active below the current mouse position
 625        x, y = rwi.GetEventPosition()
 626        self.FindPokedRenderer(x, y)
 627
 628        # The movement
 629        ren = self.GetCurrentRenderer()
 630
 631        #   // Calculate the focal depth since we'll be using it a lot
 632        camera = ren.GetActiveCamera()
 633        viewFocus = camera.GetFocalPoint()
 634
 635        temp_out = [0, 0, 0]
 636        self.ComputeWorldToDisplay(
 637            ren, viewFocus[0], viewFocus[1], viewFocus[2], temp_out
 638        )
 639        focalDepth = temp_out[2]
 640
 641        newPickPoint = [0, 0, 0, 0]
 642        x, y = rwi.GetEventPosition()
 643        self.ComputeDisplayToWorld(ren, x, y, focalDepth, newPickPoint)
 644
 645        # Has to recalc old mouse point since the viewport has moved,
 646        # so can't move it outside the loop
 647        oldPickPoint = [0, 0, 0, 0]
 648        # xp, yp = rwi.GetLastEventPosition()
 649
 650        # find the center of the window
 651        size = rwi.GetRenderWindow().GetSize()
 652        xp = size[0] / 2
 653        yp = size[1] / 2
 654
 655        self.ComputeDisplayToWorld(ren, xp, yp, focalDepth, oldPickPoint)
 656        # Camera motion is reversed
 657        move_factor = -1 * self.zoom_motion_factor * direction
 658
 659        motionVector = (
 660            move_factor * (oldPickPoint[0] - newPickPoint[0]),
 661            move_factor * (oldPickPoint[1] - newPickPoint[1]),
 662            move_factor * (oldPickPoint[2] - newPickPoint[2]),
 663        )
 664
 665        viewFocus = camera.GetFocalPoint()  # do we need to do this again? Already did this
 666        viewPoint = camera.GetPosition()
 667
 668        camera.SetFocalPoint(
 669            motionVector[0] + viewFocus[0],
 670            motionVector[1] + viewFocus[1],
 671            motionVector[2] + viewFocus[2],
 672        )
 673        camera.SetPosition(
 674            motionVector[0] + viewPoint[0],
 675            motionVector[1] + viewPoint[1],
 676            motionVector[2] + viewPoint[2],
 677        )
 678
 679        # the zooming
 680        factor = self.mouse_motion_factor * self.mouse_wheel_motion_factor
 681        self.zoom_by_step(direction * factor)
 682
 683    def zoom_by_step(self, step):
 684        if self.GetCurrentRenderer():
 685            self.StartDolly()
 686            self.dolly(pow(1.1, step))
 687            self.EndDolly()
 688
 689    def left_button_press(self, _obj, _event):
 690        if self._is_box_zooming:
 691            return
 692        if self.draginfo:
 693            return
 694
 695        self._left_button_down = True
 696
 697        interactor = self.GetInteractor()
 698        Shift = interactor.GetShiftKey()
 699        Ctrl = interactor.GetControlKey()
 700
 701        if Shift and Ctrl:
 702            if not self.GetCurrentRenderer().GetActiveCamera().GetParallelProjection():
 703                self.toggle_parallel_projection()
 704
 705        rwi = self.GetInteractor()
 706        self.start_x, self.start_y = rwi.GetEventPosition()
 707        self.end_x = self.start_x
 708        self.end_y = self.start_y
 709
 710        self.initialize_screen_drawing()
 711
 712    def left_button_release(self, _obj, _event):
 713        # print("LeftButtonRelease")
 714        if self._is_box_zooming:
 715            self._is_box_zooming = False
 716            self.zoom_box(self.start_x, self.start_y, self.end_x, self.end_y)
 717            return
 718
 719        if self.draginfo:
 720            self.finish_drag()
 721            return
 722
 723        self._left_button_down = False
 724
 725        interactor = self.GetInteractor()
 726
 727        Shift = interactor.GetShiftKey()
 728        Ctrl = interactor.GetControlKey()
 729        # Alt = interactor.GetAltKey()
 730
 731        if Ctrl and Shift:
 732            pass  # we were drawing the measurement
 733
 734        else:
 735            if self.callback_select:
 736                props = self.perform_picking_on_selection()
 737                if props:  # only call back if anything was selected
 738                    self.picked_props = tuple(props)
 739                    self.callback_select(props)
 740
 741        # remove the selection rubber band / line
 742        self.GetInteractor().Render()
 743
 744    def key_press(self, obj, _event):
 745
 746        key = obj.GetKeySym()
 747        KEY = key.upper()
 748
 749        # logging.info(f"Key Press: {key}")
 750        if self.callback_any_key:
 751            if self.callback_any_key(key):
 752                return
 753
 754        if KEY == "M":
 755            self.middle_mouse_lock = not self.middle_mouse_lock
 756            self._update_middle_mouse_button_lock_actor()
 757        elif KEY == "G":
 758            if self.draginfo is not None:
 759                self.finish_drag()
 760            else:
 761                if self.callback_start_drag:
 762                    self.callback_start_drag()
 763                else:
 764                    self.start_drag()
 765                    # internally calls end-drag if drag is already active
 766        elif KEY == "ESCAPE":
 767            if self.callback_escape_key:
 768                self.callback_escape_key()
 769            if self.draginfo is not None:
 770                self.cancel_drag()
 771        elif KEY == "DELETE":
 772            if self.callback_delete_key:
 773                self.callback_delete_key()
 774        elif KEY == "RETURN":
 775            if self.draginfo:
 776                self.finish_drag()
 777        elif KEY == "SPACE":
 778            self.middle_mouse_lock = True
 779            # self._update_middle_mouse_button_lock_actor()
 780            # self.GrabFocus("MouseMoveEvent", self)
 781            # # TODO: grab and release focus; possible from python?
 782        elif KEY == "B":
 783            self._is_box_zooming = True
 784            rwi = self.GetInteractor()
 785            self.start_x, self.start_y = rwi.GetEventPosition()
 786            self.end_x = self.start_x
 787            self.end_y = self.start_y
 788            self.initialize_screen_drawing()
 789        elif KEY in ("2", "3"):
 790            self.toggle_parallel_projection()
 791        elif KEY == "A":
 792            self.zoom_fit()
 793        elif KEY == "X":
 794            self.set_view_x()
 795        elif KEY == "Y":
 796            self.set_view_y()
 797        elif KEY == "Z":
 798            self.set_view_z()
 799        elif KEY == "LEFT":
 800            self.rotate_discrete_step(1)
 801        elif KEY == "RIGHT":
 802            self.rotate_discrete_step(-1)
 803        elif KEY == "UP":
 804            self.rotate_turtable_by(0, 10)
 805        elif KEY == "DOWN":
 806            self.rotate_turtable_by(0, -10)
 807        elif KEY == "PLUS":
 808            self.zoom_by_step(2)
 809        elif KEY == "MINUS":
 810            self.zoom_by_step(-2)
 811        elif KEY == "F":
 812            if self.callback_focus_key:
 813                self.callback_focus_key()
 814
 815        self.InvokeEvent("InteractionEvent", None)
 816
 817    def key_release(self, obj, _event):
 818        key = obj.GetKeySym()
 819        KEY = key.upper()
 820        # print(f"Key release: {key}")
 821        if KEY == "SPACE":
 822            if self.middle_mouse_lock:
 823                self.middle_mouse_lock = False
 824                self._update_middle_mouse_button_lock_actor()
 825
 826    def window_resized(self):
 827        # print("window resized")
 828        self.initialize_screen_drawing()
 829
 830    def rotate_discrete_step(self, movement_direction, step=22.5):
 831        """
 832        Rotates CW or CCW to the nearest 45 deg angle
 833        - includes some fuzzyness to determine about which axis
 834        """
 835        camera = self.GetCurrentRenderer().GetActiveCamera()
 836
 837        step = np.deg2rad(step)
 838
 839        direction = -np.array(camera.GetViewPlaneNormal())  # current camera direction
 840
 841        if abs(direction[2]) < 0.7:
 842            # horizontal view, rotate camera position about Z-axis
 843            angle = np.arctan2(direction[1], direction[0])
 844
 845            # find the nearest angle that is an integer number of steps
 846            if movement_direction > 0:
 847                angle = step * np.floor((angle + 0.1 * step) / step) + step
 848            else:
 849                angle = -step * np.floor(-(angle - 0.1 * step) / step) - step
 850
 851            dist = np.linalg.norm(direction[:2])
 852            direction[0] = np.cos(angle) * dist
 853            direction[1] = np.sin(angle) * dist
 854
 855            self.set_camera_direction(direction)
 856
 857        else:  # Top or bottom like view - rotate camera "up" direction
 858
 859            up = np.array(camera.GetViewUp())
 860            angle = np.arctan2(up[1], up[0])
 861
 862            # find the nearest angle that is an integer number of steps
 863            if movement_direction > 0:
 864                angle = step * np.floor((angle + 0.1 * step) / step) + step
 865            else:
 866                angle = -step * np.floor(-(angle - 0.1 * step) / step) - step
 867
 868            dist = np.linalg.norm(up[:2])
 869            up[0] = np.cos(angle) * dist
 870            up[1] = np.sin(angle) * dist
 871
 872            camera.SetViewUp(up)
 873            camera.OrthogonalizeViewUp()
 874            self.GetInteractor().Render()
 875
 876    def toggle_parallel_projection(self):
 877        renderer = self.GetCurrentRenderer()
 878        camera = renderer.GetActiveCamera()
 879        camera.SetParallelProjection(not bool(camera.GetParallelProjection()))
 880        self.GetInteractor().Render()
 881
 882    def set_view_x(self):
 883        self.set_camera_plane_direction((1, 0, 0))
 884
 885    def set_view_y(self):
 886        self.set_camera_plane_direction((0, 1, 0))
 887
 888    def set_view_z(self):
 889        self.set_camera_plane_direction((0, 0, 1))
 890
 891    def zoom_fit(self):
 892        self.GetCurrentRenderer().ResetCamera()
 893        self.GetInteractor().Render()
 894
 895    def set_camera_plane_direction(self, direction):
 896        """
 897        Sets the camera to display a plane of which direction is the normal
 898        - includes logic to reverse the direction if benificial
 899        """
 900        camera = self.GetCurrentRenderer().GetActiveCamera()
 901
 902        direction = np.array(direction)
 903        normal = camera.GetViewPlaneNormal()
 904        # can not set the normal, need to change the position to do that
 905
 906        current_alignment = np.dot(normal, -direction)
 907        if abs(current_alignment) > 0.9999:
 908            # print("toggling")
 909            direction = -np.array(normal)
 910        elif current_alignment > 0:  # find the nearest plane
 911            # print("reversing to find nearest")
 912            direction = -direction
 913
 914        self.set_camera_direction(-direction)
 915
 916    def set_camera_direction(self, direction):
 917        """Sets the camera to this direction, sets view up if horizontal enough"""
 918        direction = np.array(direction)
 919
 920        ren = self.GetCurrentRenderer()
 921        camera = ren.GetActiveCamera()
 922        rwi = self.GetInteractor()
 923
 924        pos = np.array(camera.GetPosition())
 925        focal = np.array(camera.GetFocalPoint())
 926        dist = np.linalg.norm(pos - focal)
 927
 928        pos = focal - dist * direction
 929        camera.SetPosition(pos)
 930
 931        if abs(direction[2]) < 0.9:
 932            camera.SetViewUp(0, 0, 1)
 933        elif direction[2] > 0.9:
 934            camera.SetViewUp(0, -1, 0)
 935        else:
 936            camera.SetViewUp(0, 1, 0)
 937
 938        camera.OrthogonalizeViewUp()
 939
 940        if self.GetAutoAdjustCameraClippingRange():
 941            ren.ResetCameraClippingRange()
 942
 943        if rwi.GetLightFollowCamera():
 944            ren.UpdateLightsGeometryToFollowCamera()
 945
 946        if self.callback_camera_direction_changed:
 947            self.callback_camera_direction_changed()
 948
 949        self.GetInteractor().Render()
 950
 951    def perform_picking_on_selection(self):
 952        """
 953        Performs 3d picking on the current dragged selection
 954
 955        If the distance between the start and endpoints is less than the threshold
 956        then a SINGLE 3d prop is picked along the line.
 957
 958        The selection area is drawn by the rubber band and is defined by
 959        `self.start_x, self.start_y, self.end_x, self.end_y`
 960        """
 961        renderer = self.GetCurrentRenderer()
 962        if not renderer:
 963            return []
 964
 965        assemblyPath = renderer.PickProp(self.start_x, self.start_y, self.end_x, self.end_y)
 966
 967        # re-pick in larger area if nothing is returned
 968        if not assemblyPath:
 969            self.start_x -= 2
 970            self.end_x += 2
 971            self.start_y -= 2
 972            self.end_y += 2
 973            assemblyPath = renderer.PickProp(self.start_x, self.start_y, self.end_x, self.end_y)
 974
 975        # The nearest prop (by Z-value)
 976        if assemblyPath:
 977            assert (
 978                assemblyPath.GetNumberOfItems() == 1
 979            ), "Wrong assumption on number of returned nodes when picking"
 980            nearest_prop = assemblyPath.GetItemAsObject(0).GetViewProp()
 981
 982            # all props
 983            collection = renderer.GetPickResultProps()
 984            props = [collection.GetItemAsObject(i) for i in range(collection.GetNumberOfItems())]
 985
 986            props.remove(nearest_prop)
 987            props.insert(0, nearest_prop)
 988            return props
 989
 990        else:
 991            return []
 992
 993    # ----------- actor dragging ------------
 994    def start_drag(self):
 995        """Starts the drag operation"""
 996        if self.callback_start_drag:
 997            # print("Calling callback_start_drag")
 998            self.callback_start_drag()
 999            return
1000        else:  # grab the current selection
1001            if self.picked_props:
1002                self.start_drag_on_props(self.picked_props)
1003            else:
1004                pass
1005                # print('Can not start drag,
1006                # nothing selected and callback_start_drag not assigned')
1007
1008    def finish_drag(self):
1009        """Finishes the drag operation"""
1010        # print('Finished drag')
1011        if self.callback_end_drag:
1012            # reset actor positions as actors positions will be controlled
1013            # by called functions
1014            for pos0, actor in zip(
1015                self.draginfo.dragged_actors_original_positions,
1016                self.draginfo.actors_dragging,
1017            ):
1018                actor.SetPosition(pos0)
1019            self.callback_end_drag(self.draginfo)
1020
1021        self.draginfo = None
1022
1023    def start_drag_on_props(self, props):
1024        """
1025        Starts drag on the provided props (actors) by filling self.draginfo"""
1026        if self.draginfo is not None:
1027            self.finish_drag()
1028            return
1029
1030        # create and fill drag-info
1031        draginfo = _BlenderStyleDragInfo()
1032        draginfo.actors_dragging = props  # [*actors, *outlines]
1033
1034        for a in draginfo.actors_dragging:
1035            draginfo.dragged_actors_original_positions.append(a.GetPosition())
1036
1037        # Get the start position of the drag in 3d
1038        rwi = self.GetInteractor()
1039        ren = self.GetCurrentRenderer()
1040        camera = ren.GetActiveCamera()
1041        viewFocus = camera.GetFocalPoint()
1042
1043        temp_out = [0, 0, 0]
1044        self.ComputeWorldToDisplay(
1045            ren, viewFocus[0], viewFocus[1], viewFocus[2], temp_out
1046        )
1047        focalDepth = temp_out[2]
1048
1049        newPickPoint = [0, 0, 0, 0]
1050        x, y = rwi.GetEventPosition()
1051        self.ComputeDisplayToWorld(ren, x, y, focalDepth, newPickPoint)
1052
1053        mouse_pos_3d = np.array(newPickPoint[:3])
1054        draginfo.start_position_3d = mouse_pos_3d
1055        self.draginfo = draginfo
1056
1057    def execute_drag(self):
1058        """Executes the drag operation"""
1059        rwi = self.GetInteractor()
1060        ren = self.GetCurrentRenderer()
1061
1062        camera = ren.GetActiveCamera()
1063        viewFocus = camera.GetFocalPoint()
1064
1065        # Get the picked point in 3d
1066        temp_out = [0, 0, 0]
1067        self.ComputeWorldToDisplay(
1068            ren, viewFocus[0], viewFocus[1], viewFocus[2], temp_out
1069        )
1070        focalDepth = temp_out[2]
1071
1072        newPickPoint = [0, 0, 0, 0]
1073        x, y = rwi.GetEventPosition()
1074        self.ComputeDisplayToWorld(ren, x, y, focalDepth, newPickPoint)
1075
1076        mouse_pos_3d = np.array(newPickPoint[:3])
1077
1078        # compute the delta and execute
1079
1080        delta = np.array(mouse_pos_3d) - self.draginfo.start_position_3d
1081        # print(f'Delta = {delta}')
1082        view_normal = np.array(ren.GetActiveCamera().GetViewPlaneNormal())
1083
1084        delta_inplane = delta - view_normal * np.dot(delta, view_normal)
1085        # print(f'delta_inplane = {delta_inplane}')
1086
1087        for pos0, actor in zip(
1088            self.draginfo.dragged_actors_original_positions,
1089            self.draginfo.actors_dragging,
1090        ):
1091            m = actor.GetUserMatrix()
1092            if m:
1093                print("UserMatrices/transforms not supported")
1094                # m.Invert() #inplace
1095                # rotated = m.MultiplyFloatPoint([*delta_inplane, 1])
1096                # actor.SetPosition(pos0 + np.array(rotated[:3]))
1097            actor.SetPosition(pos0 + delta_inplane)
1098
1099        # print(f'Set position to {pos0 + delta_inplane}')
1100        self.draginfo.delta = delta_inplane  # store the current delta
1101
1102        self.GetInteractor().Render()
1103
1104    def cancel_drag(self):
1105        """Cancels the drag and restored the original positions of all dragged actors"""
1106        for pos0, actor in zip(
1107            self.draginfo.dragged_actors_original_positions,
1108            self.draginfo.actors_dragging,
1109        ):
1110            actor.SetPosition(pos0)
1111        self.draginfo = None
1112        self.GetInteractor().Render()
1113
1114    # ----------- end dragging --------------
1115
1116    def zoom(self):
1117        """Zooms the camera"""
1118        rwi = self.GetInteractor()
1119        _, y = rwi.GetEventPosition()
1120        _, yp = rwi.GetLastEventPosition()
1121        direction = y - yp
1122        self.move_mouse_wheel(direction / 10)
1123
1124    def pan(self):
1125        """Pans the camera"""
1126        ren = self.GetCurrentRenderer()
1127
1128        if ren:
1129            rwi = self.GetInteractor()
1130
1131            #   // Calculate the focal depth since we'll be using it a lot
1132            camera = ren.GetActiveCamera()
1133            viewFocus = camera.GetFocalPoint()
1134
1135            temp_out = [0, 0, 0]
1136            self.ComputeWorldToDisplay(
1137                ren, viewFocus[0], viewFocus[1], viewFocus[2], temp_out
1138            )
1139            focalDepth = temp_out[2]
1140
1141            newPickPoint = [0, 0, 0, 0]
1142            x, y = rwi.GetEventPosition()
1143            self.ComputeDisplayToWorld(ren, x, y, focalDepth, newPickPoint)
1144
1145            # Has to recalc old mouse point since the viewport has moved,
1146            # so can't move it outside the loop
1147
1148            oldPickPoint = [0, 0, 0, 0]
1149            xp, yp = rwi.GetLastEventPosition()
1150            self.ComputeDisplayToWorld(ren, xp, yp, focalDepth, oldPickPoint)
1151            #
1152            # Camera motion is reversed
1153            motionVector = (
1154                oldPickPoint[0] - newPickPoint[0],
1155                oldPickPoint[1] - newPickPoint[1],
1156                oldPickPoint[2] - newPickPoint[2],
1157            )
1158
1159            viewFocus = camera.GetFocalPoint()  # do we need to do this again? Already did this
1160            viewPoint = camera.GetPosition()
1161
1162            camera.SetFocalPoint(
1163                motionVector[0] + viewFocus[0],
1164                motionVector[1] + viewFocus[1],
1165                motionVector[2] + viewFocus[2],
1166            )
1167            camera.SetPosition(
1168                motionVector[0] + viewPoint[0],
1169                motionVector[1] + viewPoint[1],
1170                motionVector[2] + viewPoint[2],
1171            )
1172
1173            if rwi.GetLightFollowCamera():
1174                ren.UpdateLightsGeometryToFollowCamera()
1175
1176            self.GetInteractor().Render()
1177
1178    def rotate(self):
1179        """Rotates the camera"""
1180        ren = self.GetCurrentRenderer()
1181
1182        if ren:
1183
1184            rwi = self.GetInteractor()
1185            dx = rwi.GetEventPosition()[0] - rwi.GetLastEventPosition()[0]
1186            dy = rwi.GetEventPosition()[1] - rwi.GetLastEventPosition()[1]
1187
1188            size = ren.GetRenderWindow().GetSize()
1189            delta_elevation = -20.0 / size[1]
1190            delta_azimuth = -20.0 / size[0]
1191
1192            rxf = dx * delta_azimuth * self.mouse_motion_factor
1193            ryf = dy * delta_elevation * self.mouse_motion_factor
1194
1195            self.rotate_turtable_by(rxf, ryf)
1196
1197    def rotate_turtable_by(self, rxf, ryf):
1198        """Rotates the camera in turn-table mode"""
1199        ren = self.GetCurrentRenderer()
1200        rwi = self.GetInteractor()
1201
1202        # rfx is rotation about the global Z vector (turn-table mode)
1203        # rfy is rotation about the side vector
1204
1205        camera = ren.GetActiveCamera()
1206        campos = np.array(camera.GetPosition())
1207        focal = np.array(camera.GetFocalPoint())
1208        up = camera.GetViewUp()
1209        upside_down_factor = -1 if up[2] < 0 else 1
1210
1211        # rotate about focal point
1212        P = campos - focal  # camera position
1213
1214        # Rotate left/right about the global Z axis
1215        H = np.linalg.norm(P[:2])  # horizontal distance of camera to focal point
1216        elev = np.arctan2(P[2], H)  # elevation
1217
1218        # if the camera is near the poles, then derive the azimuth from the up-vector
1219        sin_elev = np.sin(elev)
1220        if abs(sin_elev) < 0.8:
1221            azi = np.arctan2(P[1], P[0])  # azimuth from camera position
1222        else:
1223            if sin_elev < -0.8:
1224                azi = np.arctan2(upside_down_factor * up[1], upside_down_factor * up[0])
1225            else:
1226                azi = np.arctan2(-upside_down_factor * up[1], -upside_down_factor * up[0])
1227
1228        D = np.linalg.norm(P)  # distance from focal point to camera
1229
1230        # apply the change in azimuth and elevation
1231        azi_new = azi + rxf / 60
1232        elev_new = elev + upside_down_factor * ryf / 60
1233
1234        # the changed elevation changes H (D stays the same)
1235        Hnew = D * np.cos(elev_new)
1236
1237        # calculate new camera position relative to focal point
1238        Pnew = np.array((Hnew * np.cos(azi_new), Hnew * np.sin(azi_new), D * np.sin(elev_new)))
1239
1240        # calculate the up-direction of the camera
1241        up_z = upside_down_factor * np.cos(elev_new)  # z follows directly from elevation
1242        up_h = upside_down_factor * np.sin(elev_new)  # horizontal component
1243        #
1244        # if upside_down:
1245        #     up_z = -up_z
1246        #     up_h = -up_h
1247        up = (-up_h * np.cos(azi_new), -up_h * np.sin(azi_new), up_z)
1248        new_pos = focal + Pnew
1249        camera.SetViewUp(up)
1250        camera.SetPosition(new_pos)
1251        camera.OrthogonalizeViewUp()
1252
1253        # Update
1254
1255        if self.GetAutoAdjustCameraClippingRange():
1256            ren.ResetCameraClippingRange()
1257
1258        if rwi.GetLightFollowCamera():
1259            ren.UpdateLightsGeometryToFollowCamera()
1260
1261        if self.callback_camera_direction_changed:
1262            self.callback_camera_direction_changed()
1263
1264        self.GetInteractor().Render()
1265
1266    def zoom_box(self, x1, y1, x2, y2):
1267        """Zooms to a box"""
1268        if x1 > x2:
1269            #swap x1 and x2
1270            x1, x2 = x2, x1
1271        if y1 > y2:
1272            y1, y2 = y2, y1
1273
1274        width = x2 - x1
1275        height = y2 - y1
1276
1277        ren = self.GetCurrentRenderer()
1278        size = ren.GetSize()
1279        origin = ren.GetOrigin()
1280        camera = ren.GetActiveCamera()
1281
1282        # Assuming we're drawing the band on the view-plane
1283        rbcenter = (x1 + width / 2, y1 + height / 2, 0)
1284
1285        ren.SetDisplayPoint(rbcenter)
1286        ren.DisplayToView()
1287        ren.ViewToWorld()
1288
1289        worldRBCenter = ren.GetWorldPoint()
1290
1291        invw = 1.0 / worldRBCenter[3]
1292        worldRBCenter = [c * invw for c in worldRBCenter]
1293        winCenter = [origin[0] + 0.5 * size[0], origin[1] + 0.5 * size[1], 0]
1294
1295        ren.SetDisplayPoint(winCenter)
1296        ren.DisplayToView()
1297        ren.ViewToWorld()
1298
1299        worldWinCenter = ren.GetWorldPoint()
1300        invw = 1.0 / worldWinCenter[3]
1301        worldWinCenter = [c * invw for c in worldWinCenter]
1302
1303        translation = [
1304            worldRBCenter[0] - worldWinCenter[0],
1305            worldRBCenter[1] - worldWinCenter[1],
1306            worldRBCenter[2] - worldWinCenter[2],
1307        ]
1308
1309        pos = camera.GetPosition()
1310        fp = camera.GetFocalPoint()
1311        #
1312        pos = [pos[i] + translation[i] for i in range(3)]
1313        fp = [fp[i] + translation[i] for i in range(3)]
1314
1315        #
1316        camera.SetPosition(pos)
1317        camera.SetFocalPoint(fp)
1318
1319        if width > height:
1320            if width:
1321                camera.Zoom(size[0] / width)
1322        else:
1323            if height:
1324                camera.Zoom(size[1] / height)
1325
1326        self.GetInteractor().Render()
1327
1328    def focus_on(self, prop3D):
1329        """Move the camera to focus on this particular prop3D"""
1330
1331        position = prop3D.GetPosition()
1332
1333        ren = self.GetCurrentRenderer()
1334        camera = ren.GetActiveCamera()
1335
1336        fp = camera.GetFocalPoint()
1337        pos = camera.GetPosition()
1338
1339        camera.SetFocalPoint(position)
1340        camera.SetPosition(
1341            position[0] - fp[0] + pos[0],
1342            position[1] - fp[1] + pos[1],
1343            position[2] - fp[2] + pos[2],
1344        )
1345
1346        if self.GetAutoAdjustCameraClippingRange():
1347            ren.ResetCameraClippingRange()
1348
1349        rwi = self.GetInteractor()
1350        if rwi.GetLightFollowCamera():
1351            ren.UpdateLightsGeometryToFollowCamera()
1352
1353        self.GetInteractor().Render()
1354
1355    def dolly(self, factor):
1356        """Dolly the camera"""
1357        ren = self.GetCurrentRenderer()
1358
1359        if ren:
1360            camera = ren.GetActiveCamera()
1361
1362            if camera.GetParallelProjection():
1363                camera.SetParallelScale(camera.GetParallelScale() / factor)
1364            else:
1365                camera.Dolly(factor)
1366                if self.GetAutoAdjustCameraClippingRange():
1367                    ren.ResetCameraClippingRange()
1368
1369            # if not do_not_update:
1370            #     rwi = self.GetInteractor()
1371            #     if rwi.GetLightFollowCamera():
1372            #         ren.UpdateLightsGeometryToFollowCamera()
1373            #     # rwi.Render()
1374
1375    def draw_measurement(self):
1376        """Draws a measurement line"""
1377        rwi = self.GetInteractor()
1378        self.end_x, self.end_y = rwi.GetEventPosition()
1379        self.draw_line(self.start_x, self.end_x, self.start_y, self.end_y)
1380
1381    def draw_dragged_selection(self):
1382        """Draws the selection rubber band"""
1383        rwi = self.GetInteractor()
1384        self.end_x, self.end_y = rwi.GetEventPosition()
1385        self.draw_rubber_band(self.start_x, self.end_x, self.start_y, self.end_y)
1386
1387    def initialize_screen_drawing(self):
1388        """Initializes the screen drawing"""
1389        # make an image of the currently rendered image
1390
1391        rwi = self.GetInteractor()
1392        rwin = rwi.GetRenderWindow()
1393
1394        size = rwin.GetSize()
1395
1396        self._pixel_array.Initialize()
1397        self._pixel_array.SetNumberOfComponents(4)
1398        self._pixel_array.SetNumberOfTuples(size[0] * size[1])
1399
1400        front = 1  # what does this do?
1401        rwin.GetRGBACharPixelData(0, 0, size[0] - 1, size[1] - 1, front, self._pixel_array)
1402
1403    def draw_rubber_band(self, x1, x2, y1, y2):
1404        """Draws a rubber band"""
1405        rwi = self.GetInteractor()
1406        rwin = rwi.GetRenderWindow()
1407
1408        size = rwin.GetSize()
1409
1410        tempPA = vtki.vtkUnsignedCharArray()
1411        tempPA.DeepCopy(self._pixel_array)
1412
1413        # check size, viewport may have been resized in the mean-time
1414        if tempPA.GetNumberOfTuples() != size[0] * size[1]:
1415            # print(
1416            #     "Starting new screen-image - viewport has resized without us knowing"
1417            # )
1418            self.initialize_screen_drawing()
1419            self.draw_rubber_band(x1, x2, y1, y2)
1420            return
1421
1422        x2 = min(x2, size[0] - 1)
1423        y2 = min(y2, size[1] - 1)
1424
1425        x2 = max(x2, 0)
1426        y2 = max(y2, 0)
1427
1428        # Modify the pixel array
1429        width = abs(x2 - x1)
1430        height = abs(y2 - y1)
1431        minx = min(x2, x1)
1432        miny = min(y2, y1)
1433
1434        # draw top and bottom
1435        for i in range(width):
1436
1437            # c = round((10*i % 254)/254) * 254  # find some alternating color
1438            c = 0
1439
1440            idx = (miny * size[0]) + minx + i
1441            tempPA.SetTuple(idx, (c, c, c, 1))
1442
1443            idx = ((miny + height) * size[0]) + minx + i
1444            tempPA.SetTuple(idx, (c, c, c, 1))
1445
1446        # draw left and right
1447        for i in range(height):
1448            # c = round((10 * i % 254) / 254) * 254  # find some alternating color
1449            c = 0
1450
1451            idx = ((miny + i) * size[0]) + minx
1452            tempPA.SetTuple(idx, (c, c, c, 1))
1453
1454            idx = idx + width
1455            tempPA.SetTuple(idx, (c, c, c, 1))
1456
1457        # and Copy back to the window
1458        rwin.SetRGBACharPixelData(0, 0, size[0] - 1, size[1] - 1, tempPA, 0)
1459        rwin.Frame()
1460
1461    def line2pixels(self, x1, x2, y1, y2):
1462        """Returns the x and y values of the pixels on a line between x1,y1 and x2,y2.
1463        If start and end are identical then a single point is returned"""
1464
1465        dx = x2 - x1
1466        dy = y2 - y1
1467
1468        if dx == 0 and dy == 0:
1469            return [x1], [y1]
1470
1471        if abs(dx) > abs(dy):
1472            dhdw = dy / dx
1473            r = range(0, dx, int(dx / abs(dx)))
1474            x = [x1 + i for i in r]
1475            y = [round(y1 + dhdw * i) for i in r]
1476        else:
1477            dwdh = dx / dy
1478            r = range(0, dy, int(dy / abs(dy)))
1479            y = [y1 + i for i in r]
1480            x = [round(x1 + i * dwdh) for i in r]
1481
1482        return x, y
1483
1484    def draw_line(self, x1, x2, y1, y2):
1485        rwi = self.GetInteractor()
1486        rwin = rwi.GetRenderWindow()
1487
1488        size = rwin.GetSize()
1489
1490        x1 = min(max(x1, 0), size[0])
1491        x2 = min(max(x2, 0), size[0])
1492        y1 = min(max(y1, 0), size[1])
1493        y2 = min(max(y2, 0), size[1])
1494
1495        tempPA = vtki.vtkUnsignedCharArray()
1496        tempPA.DeepCopy(self._pixel_array)
1497
1498        xs, ys = self.line2pixels(x1, x2, y1, y2)
1499        for x, y in zip(xs, ys):
1500            idx = (y * size[0]) + x
1501            tempPA.SetTuple(idx, (0, 0, 0, 1))
1502
1503        # and Copy back to the window
1504        rwin.SetRGBACharPixelData(0, 0, size[0] - 1, size[1] - 1, tempPA, 0)
1505
1506        camera = self.GetCurrentRenderer().GetActiveCamera()
1507        scale = camera.GetParallelScale()
1508
1509        # Set/Get the scaling used for a parallel projection, i.e.
1510        #
1511        # the half of the height of the viewport in world-coordinate distances.
1512        # The default is 1. Note that the "scale" parameter works as an "inverse scale"
1513        #  larger numbers produce smaller images.
1514        # This method has no effect in perspective projection mode
1515
1516        half_height = size[1] / 2
1517        # half_height [px] = scale [world-coordinates]
1518
1519        length = ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5
1520        meters_per_pixel = scale / half_height
1521        meters = length * meters_per_pixel
1522
1523        if camera.GetParallelProjection():
1524            print(f"Line length = {length} px = {meters} m")
1525        else:
1526            print("Need to be in non-perspective mode to measure. Press 2 or 3 to get there")
1527
1528        if self.callback_measure:
1529            self.callback_measure(meters)
1530
1531        # # can we add something to the window here?
1532        # freeType = vtk.FreeTypeTools.GetInstance()
1533        # textProperty = vtki.vtkTextProperty()
1534        # textProperty.SetJustificationToLeft()
1535        # textProperty.SetFontSize(24)
1536        # textProperty.SetOrientation(25)
1537        #
1538        # textImage = vtki.vtkImageData()
1539        # freeType.RenderString(textProperty, "a somewhat longer text", 72, textImage)
1540        # # this does not give an error, assume it works
1541        # #
1542        # textImage.GetDimensions()
1543        # textImage.GetExtent()
1544        #
1545        # # # Now put the textImage in the RenderWindow
1546        # rwin.SetRGBACharPixelData(0, 0, size[0] - 1, size[1] - 1, textImage, 0)
1547
1548        rwin.Frame()
1549
1550    def _update_middle_mouse_button_lock_actor(self):
1551
1552        if self.middle_mouse_lock_actor is None:
1553            # create the actor
1554            # Create a text on the top-rightcenter
1555            textMapper = vtki.new("TextMapper")
1556            textMapper.SetInput("Middle mouse lock [m or space] active")
1557            textProp = textMapper.GetTextProperty()
1558            textProp.SetFontSize(12)
1559            textProp.SetFontFamilyToTimes()
1560            textProp.BoldOff()
1561            textProp.ItalicOff()
1562            textProp.ShadowOff()
1563            textProp.SetVerticalJustificationToTop()
1564            textProp.SetJustificationToCentered()
1565            textProp.SetColor((0, 0, 0))
1566
1567            self.middle_mouse_lock_actor = vtki.vtkActor2D()
1568            self.middle_mouse_lock_actor.SetMapper(textMapper)
1569            self.middle_mouse_lock_actor.GetPositionCoordinate().SetCoordinateSystemToNormalizedDisplay()
1570            self.middle_mouse_lock_actor.GetPositionCoordinate().SetValue(0.5, 0.98)
1571
1572            self.GetCurrentRenderer().AddActor(self.middle_mouse_lock_actor)
1573
1574        self.middle_mouse_lock_actor.SetVisibility(self.middle_mouse_lock)
1575        self.GetInteractor().Render()
class MousePan(vtkmodules.vtkInteractionStyle.vtkInteractorStyleUser):
 15class MousePan(vtki.vtkInteractorStyleUser):
 16    """
 17    Interaction mode to pan the scene by dragging the mouse.
 18
 19    Controls:
 20    - Left mouse button will pan the scene.
 21    - Mouse middle button up/down is elevation, and left and right is azimuth.
 22    - Right mouse button is rotate (left/right movement) and zoom in/out (up/down movement)
 23    - Mouse scroll wheel is zoom in/out
 24
 25    Arguments:
 26        enable_pan: (bool)
 27            Enable panning the scene.
 28        enable_zoom: (bool)
 29            Enable zooming the scene.
 30        enable_rotate: (bool)
 31            Enable rotating the scene.
 32        speed: (float)
 33            Speed factor of the interaction, e.i. how fast the scene must move.
 34    """
 35
 36    def __init__(self, enable_pan=True, enable_zoom=True, enable_rotate=True, speed=1.0):
 37
 38        super().__init__()
 39
 40        self.left = False
 41        self.middle = False
 42        self.right = False
 43        self.speed = speed
 44
 45        self.interactor = None
 46        self.renderer = None
 47        self.camera = None
 48
 49        self.oldpickD = []
 50        self.newpickD = []
 51        self.oldpickW = np.array([0, 0, 0, 0], dtype=float)
 52        self.newpickW = np.array([0, 0, 0, 0], dtype=float)
 53        self.fpD = np.array([0, 0, 0], dtype=float)
 54        self.fpW = np.array([0, 0, 0], dtype=float)
 55        self.posW = np.array([0, 0, 0], dtype=float)
 56        self.motionD = np.array([0, 0], dtype=float)
 57        self.motionW = np.array([0, 0, 0], dtype=float)
 58
 59        # print("MousePan: Left mouse button to pan the scene.")
 60        self.AddObserver("LeftButtonPressEvent", self._left_down)
 61        self.AddObserver("LeftButtonReleaseEvent", self._left_up)
 62
 63        if enable_rotate:
 64            self.AddObserver("MiddleButtonPressEvent", self._middle_down)
 65            self.AddObserver("MiddleButtonReleaseEvent", self._middle_up)
 66
 67        if enable_zoom:
 68            self.AddObserver("RightButtonPressEvent", self._right_down)
 69            self.AddObserver("RightButtonReleaseEvent", self._right_up)
 70            self.AddObserver("MouseWheelForwardEvent", self._wheel_forward)
 71            self.AddObserver("MouseWheelBackwardEvent", self._wheel_backward)
 72
 73        if enable_pan:
 74            self.AddObserver("MouseMoveEvent", self._mouse_move)
 75
 76    def _get_motion(self):
 77        self.oldpickD = np.array(self.interactor.GetLastEventPosition())
 78        self.newpickD = np.array(self.interactor.GetEventPosition())
 79        self.motionD = (self.newpickD - self.oldpickD) / 4
 80        self.camera = self.renderer.GetActiveCamera()
 81        self.fpW = self.camera.GetFocalPoint()
 82        self.posW = self.camera.GetPosition()
 83        self.ComputeWorldToDisplay(self.renderer, self.fpW[0], self.fpW[1], self.fpW[2], self.fpD)
 84        focaldepth = self.fpD[2]
 85        self.ComputeDisplayToWorld(
 86            self.renderer, self.oldpickD[0], self.oldpickD[1], focaldepth, self.oldpickW
 87        )
 88        self.ComputeDisplayToWorld(
 89            self.renderer, self.newpickD[0], self.newpickD[1], focaldepth, self.newpickW
 90        )
 91        self.motionW[:3] = (self.oldpickW[:3] - self.newpickW[:3]) * self.speed
 92
 93    def _mouse_left_move(self):
 94        self._get_motion()
 95        self.camera.SetFocalPoint(self.fpW[:3] + self.motionW[:3])
 96        self.camera.SetPosition(self.posW[:3] + self.motionW[:3])
 97        self.interactor.Render()
 98
 99    def _mouse_middle_move(self):
100        self._get_motion()
101        if abs(self.motionD[0]) > abs(self.motionD[1]):
102            self.camera.Azimuth(-2 * self.speed * self.motionD[0])
103        else:
104            self.camera.Elevation(-self.speed *self.motionD[1])
105        self.interactor.Render()
106
107    def _mouse_right_move(self):
108        self._get_motion()
109        if abs(self.motionD[0]) > abs(self.motionD[1]):
110            self.camera.Azimuth(-2.0 * self.speed * self.motionD[0])
111        else:
112            self.camera.Zoom(1 + self.motionD[1] / 100)
113        self.interactor.Render()
114
115    def _mouse_wheel_forward(self):
116        self.camera = self.renderer.GetActiveCamera()
117        self.camera.Zoom(1.1 * self.speed)
118        self.interactor.Render()
119
120    def _mouse_wheel_backward(self):
121        self.camera = self.renderer.GetActiveCamera()
122        self.camera.Zoom(0.9 * self.speed)
123        self.interactor.Render()
124
125    def _left_down(self, _w, _e):
126        self.left = True
127
128    def _left_up(self, _w, _e):
129        self.left = False
130
131    def _middle_down(self, _w, _e):
132        self.middle = True
133
134    def _middle_up(self, _w, _e):
135        self.middle = False
136
137    def _right_down(self, _w, _e):
138        self.right = True
139
140    def _right_up(self, _w, _e):
141        self.right = False
142
143    def _wheel_forward(self, _w, _e):
144        self._mouse_wheel_forward()
145
146    def _wheel_backward(self, _w, _e):
147        self._mouse_wheel_backward()
148
149    def _mouse_move(self, _w, _e):
150        if self.left:
151            self._mouse_left_move()
152        if self.middle:
153            self._mouse_middle_move()
154        if self.right:
155            self._mouse_right_move()

Interaction mode to pan the scene by dragging the mouse.

Controls:

  • Left mouse button will pan the scene.
  • Mouse middle button up/down is elevation, and left and right is azimuth.
  • Right mouse button is rotate (left/right movement) and zoom in/out (up/down movement)
  • Mouse scroll wheel is zoom in/out
Arguments:
  • enable_pan: (bool) Enable panning the scene.
  • enable_zoom: (bool) Enable zooming the scene.
  • enable_rotate: (bool) Enable rotating the scene.
  • speed: (float) Speed factor of the interaction, e.i. how fast the scene must move.
MousePan(enable_pan=True, enable_zoom=True, enable_rotate=True, speed=1.0)
36    def __init__(self, enable_pan=True, enable_zoom=True, enable_rotate=True, speed=1.0):
37
38        super().__init__()
39
40        self.left = False
41        self.middle = False
42        self.right = False
43        self.speed = speed
44
45        self.interactor = None
46        self.renderer = None
47        self.camera = None
48
49        self.oldpickD = []
50        self.newpickD = []
51        self.oldpickW = np.array([0, 0, 0, 0], dtype=float)
52        self.newpickW = np.array([0, 0, 0, 0], dtype=float)
53        self.fpD = np.array([0, 0, 0], dtype=float)
54        self.fpW = np.array([0, 0, 0], dtype=float)
55        self.posW = np.array([0, 0, 0], dtype=float)
56        self.motionD = np.array([0, 0], dtype=float)
57        self.motionW = np.array([0, 0, 0], dtype=float)
58
59        # print("MousePan: Left mouse button to pan the scene.")
60        self.AddObserver("LeftButtonPressEvent", self._left_down)
61        self.AddObserver("LeftButtonReleaseEvent", self._left_up)
62
63        if enable_rotate:
64            self.AddObserver("MiddleButtonPressEvent", self._middle_down)
65            self.AddObserver("MiddleButtonReleaseEvent", self._middle_up)
66
67        if enable_zoom:
68            self.AddObserver("RightButtonPressEvent", self._right_down)
69            self.AddObserver("RightButtonReleaseEvent", self._right_up)
70            self.AddObserver("MouseWheelForwardEvent", self._wheel_forward)
71            self.AddObserver("MouseWheelBackwardEvent", self._wheel_backward)
72
73        if enable_pan:
74            self.AddObserver("MouseMoveEvent", self._mouse_move)
class FlyOverSurface(vtkmodules.vtkInteractionStyle.vtkInteractorStyleUser):
159class FlyOverSurface(vtki.vtkInteractorStyleUser):
160    """
161    Interaction mode to fly over a surface.
162
163    Controls:
164        - Press arrows to move the camera in the plane of the surface.
165        - "t" (or "PageUp") will move the camera upper in z.
166        - "g" (or "PageDown") will move the camera lower in z.
167        - "x" and "X" will reset the camera to the default position towards positive or negative x.
168        - "y" and "Y" will reset the camera to the default position towards positive or negative y.
169        - "." and "," will rotate azimuth to the right or left.
170        - "r" will reset the camera to the default position.
171
172    Left button: Select a point on the surface to focus the camera on it.
173    """
174
175    def __init__(self, move_step=0.05, angle_step=1.5):
176        """
177        Interaction mode to fly over a surface.
178
179        Arguments:
180            move_step: float, optional
181                The step size for moving the camera in the plane of the surface.
182            angle_step: float, optional
183                The step size for rotating the camera.
184
185        Example:
186            [interaction_modes3.py](https://github.com/marcomusy/vedo/blob/master/examples/basic/interaction_modes3.py)
187        """
188
189        super().__init__()
190
191        self.interactor = None  # filled in plotter.py
192        self.renderer = None  # filled in plotter.py
193
194        self.angle_step = angle_step
195        self.move_step = move_step
196
197        self.tleft = vtki.vtkTransform()
198        self.tleft.RotateZ(self.angle_step)
199
200        self.tright = vtki.vtkTransform()
201        self.tright.RotateZ(-self.angle_step)
202
203        self.bounds = ()
204
205        self.AddObserver("KeyPressEvent", self._key)
206        self.AddObserver("RightButtonPressEvent", self._right_button_press)
207        self.AddObserver("MouseWheelForwardEvent", self._mouse_wheel_forward)
208        self.AddObserver("MouseWheelBackwardEvent", self._mouse_wheel_backward)
209        self.AddObserver("LeftButtonPressEvent", self._left_button_press)
210
211    @property
212    def camera(self):
213        """Return the active camera."""
214        return self.renderer.GetActiveCamera()
215
216    @property
217    def position(self):
218        """Return the camera position."""
219        return np.array(self.camera.GetPosition())
220
221    @position.setter
222    def position(self, value):
223        """Set the camera position."""
224        self.camera.SetPosition(value[:3])
225        self.camera.SetViewUp(0.00001, 0, 1)
226        self.renderer.ResetCameraClippingRange()
227
228    @property
229    def focal_point(self):
230        """Return the camera focal point."""
231        return np.array(self.camera.GetFocalPoint())
232
233    @focal_point.setter
234    def focal_point(self, value):
235        """Set the camera focal point."""
236        self.camera.SetFocalPoint(value[:3])
237        self.camera.SetViewUp(0.00001, 0, 1)
238        self.renderer.ResetCameraClippingRange()
239
240    def _right_button_press(self, _obj, event):
241        """Right button press."""
242        # print("Right button", event)
243        self._key("Down", event)
244
245    def _left_button_press(self, obj, _event):
246        """Left button press."""
247        # print("Left button", event)
248        newPickPoint = [0, 0, 0, 0]
249        focalDepth = 0
250        x, y = obj.interactor.GetEventPosition()
251        self.ComputeDisplayToWorld(self.renderer, x, y, focalDepth, newPickPoint)
252        self.focal_point = np.array(newPickPoint)
253        self.interactor.Render()
254
255    def _mouse_wheel_backward(self, _obj, event):
256        """Mouse wheel backward."""
257        # print("mouse_wheel_backward ", event)
258        self._key("Down", event)
259
260    def _mouse_wheel_forward(self, _obj, event):
261        """Mouse wheel forward."""
262        # print("mouse_wheel_forward ", event)
263        self._key("Up", event)
264
265    def _key(self, obj, event):
266        """Key press."""
267        if "Mouse" in event or "Button" in event:
268            k = obj
269        else:
270            k = obj.GetKeySym()
271            if obj.GetShiftKey():
272                k = k.upper()
273        # print("FlyOverSurface. Key press", k)
274
275        if k in ["r", "x"]:
276            # print("r pressed, reset camera")
277            self.bounds = self.renderer.ComputeVisiblePropBounds()
278            x0, x1, y0, y1, z0, z1 = self.bounds
279            dx = x1 - x0
280            z = max(z1 * 1, z0 + (y1 - y0) / 4, z0 + (x1 - x0) / 4)
281            self.position = [x0 - dx, (y0 + y1) / 2, z]
282            self.focal_point = [x0 + dx / 2, (y0 + y1) / 2, z]
283        elif k in ["X"]:
284            # print("X pressed, reset camera")
285            self.bounds = self.renderer.ComputeVisiblePropBounds()
286            x0, x1, y0, y1, z0, z1 = self.bounds
287            dx = x1 - x0
288            z = max(z1 * 1, (y1 - y0) / 4, (x1 - x0) / 4)
289            self.position = [x1 + dx, (y0 + y1) / 2, z]
290            self.focal_point = [x0 - dx / 2, (y0 + y1) / 2, z]
291
292        elif k in ["y"]:
293            # print("y pressed, reset camera")
294            self.bounds = self.renderer.ComputeVisiblePropBounds()
295            x0, x1, y0, y1, z0, z1 = self.bounds
296            dy = y1 - y0
297            z = max(z1 * 1, z0 + (y1 - y0) / 4, z0 + (x1 - x0) / 4)
298            self.position = [(x0 + x1) / 2, y0 - dy, z]
299            self.focal_point = [(x0 + x1) / 2, y1 + dy / 2, z]
300        elif k in ["Y"]:
301            # print("Y pressed, reset camera")
302            self.bounds = self.renderer.ComputeVisiblePropBounds()
303            x0, x1, y0, y1, z0, z1 = self.bounds
304            dy = y1 - y0
305            z = max(z1 * 1, z0 + (y1 - y0) / 4, z0 + (x1 - x0) / 4)
306            self.position = [(x0 + x1) / 2, y1 + dy, z]
307            self.focal_point = [(x0 + x1) / 2, y0 - dy / 2, z]
308
309        elif k in ["Up", "w"]:
310            # print("Up pressed, move forward")
311            self.bounds = self.renderer.ComputeVisiblePropBounds()
312            diagonal = np.linalg.norm(np.array(self.bounds[1::2]) - np.array(self.bounds[::2]))
313            dx = self.move_step * diagonal
314            p = np.array(self.camera.GetPosition())
315            v = np.array(self.camera.GetDirectionOfProjection())
316            newp = p + dx * v
317            self.position = [newp[0], newp[1], p[2]]
318            self.focal_point = self.focal_point + dx * v
319        elif k in ["Down", "s"]:
320            # print("Down pressed, move backward")
321            self.bounds = self.renderer.ComputeVisiblePropBounds()
322            diagonal = np.linalg.norm(np.array(self.bounds[1::2]) - np.array(self.bounds[::2]))
323            dx = self.move_step * diagonal
324            p = np.array(self.camera.GetPosition())
325            v = np.array(self.camera.GetDirectionOfProjection())
326            newp = p - dx * v
327            self.position = [newp[0], newp[1], p[2]]
328            self.focal_point = self.focal_point - dx * v
329
330        elif k in ["Left", "a"]:
331            # print("Left pressed, rotate to the left")
332            self.bounds = self.renderer.ComputeVisiblePropBounds()
333            diagonal = np.linalg.norm(np.array(self.bounds[1::2]) - np.array(self.bounds[::2]))
334            w = np.array(self.camera.GetDirectionOfProjection())
335            p = np.array(self.camera.GetPosition())
336            w2 = np.array(self.tleft.TransformFloatPoint(w))
337            self.focal_point = self.focal_point + np.linalg.norm(p-self.focal_point) * w2
338
339        elif k in ["Right", "d"]:
340            # print("Right pressed, rotate to the right")
341            self.bounds = self.renderer.ComputeVisiblePropBounds()
342            diagonal = np.linalg.norm(np.array(self.bounds[1::2]) - np.array(self.bounds[::2]))
343            w = np.array(self.camera.GetDirectionOfProjection())
344            p = np.array(self.camera.GetPosition())
345            w2 = np.array(self.tright.TransformFloatPoint(w))
346            self.focal_point = self.focal_point + np.linalg.norm(p-self.focal_point) * w2
347
348        elif k in ["t", "Prior"]:
349            # print("t pressed, move z up")
350            self.bounds = self.renderer.ComputeVisiblePropBounds()
351            diagonal = np.linalg.norm(np.array(self.bounds[1::2]) - np.array(self.bounds[::2]))
352            dx = self.move_step * diagonal
353            p = self.position
354            self.position = [p[0], p[1], p[2] + dx / 4]
355
356        elif k in ["g", "Next"]:
357            # print("g pressed, move z down")
358            self.bounds = self.renderer.ComputeVisiblePropBounds()
359            diagonal = np.linalg.norm(np.array(self.bounds[1::2]) - np.array(self.bounds[::2]))
360            dx = self.move_step * diagonal
361            p = self.position
362            self.position = [p[0], p[1], p[2] - dx / 4]
363
364        elif k in ["comma", "COMMA"]:
365            # print("< pressed, rotate azimuth to the left")
366            self.bounds = self.renderer.ComputeVisiblePropBounds()
367            scene_center = [
368                self.bounds[0] + self.bounds[1],
369                self.bounds[2] + self.bounds[3],
370                self.bounds[4] + self.bounds[5],
371            ]
372            scene_center = np.array(scene_center) / 2
373            p = self.position
374            v = p - scene_center
375            newp = scene_center + self.tleft.TransformFloatPoint(v)
376            self.position = [newp[0], newp[1], p[2]]
377
378        elif k in ["period", "PERIOD"]:
379            # print("< pressed, rotate azimuth to the right")
380            self.bounds = self.renderer.ComputeVisiblePropBounds()
381            scene_center = [
382                self.bounds[0] + self.bounds[1],
383                self.bounds[2] + self.bounds[3],
384                self.bounds[4] + self.bounds[5],
385            ]
386            scene_center = np.array(scene_center) / 2
387            p = self.position
388            v = p - scene_center
389            newp = scene_center + self.tright.TransformFloatPoint(v)
390            self.position = [newp[0], newp[1], p[2]]
391
392        elif k in ["q", "Return"]:
393            self.interactor.ExitCallback()
394            return
395
396        else:
397            return
398
399        self.interactor.Render()

Interaction mode to fly over a surface.

Controls:
  • Press arrows to move the camera in the plane of the surface.
  • "t" (or "PageUp") will move the camera upper in z.
  • "g" (or "PageDown") will move the camera lower in z.
  • "x" and "X" will reset the camera to the default position towards positive or negative x.
  • "y" and "Y" will reset the camera to the default position towards positive or negative y.
  • "." and "," will rotate azimuth to the right or left.
  • "r" will reset the camera to the default position.

Left button: Select a point on the surface to focus the camera on it.

FlyOverSurface(move_step=0.05, angle_step=1.5)
175    def __init__(self, move_step=0.05, angle_step=1.5):
176        """
177        Interaction mode to fly over a surface.
178
179        Arguments:
180            move_step: float, optional
181                The step size for moving the camera in the plane of the surface.
182            angle_step: float, optional
183                The step size for rotating the camera.
184
185        Example:
186            [interaction_modes3.py](https://github.com/marcomusy/vedo/blob/master/examples/basic/interaction_modes3.py)
187        """
188
189        super().__init__()
190
191        self.interactor = None  # filled in plotter.py
192        self.renderer = None  # filled in plotter.py
193
194        self.angle_step = angle_step
195        self.move_step = move_step
196
197        self.tleft = vtki.vtkTransform()
198        self.tleft.RotateZ(self.angle_step)
199
200        self.tright = vtki.vtkTransform()
201        self.tright.RotateZ(-self.angle_step)
202
203        self.bounds = ()
204
205        self.AddObserver("KeyPressEvent", self._key)
206        self.AddObserver("RightButtonPressEvent", self._right_button_press)
207        self.AddObserver("MouseWheelForwardEvent", self._mouse_wheel_forward)
208        self.AddObserver("MouseWheelBackwardEvent", self._mouse_wheel_backward)
209        self.AddObserver("LeftButtonPressEvent", self._left_button_press)

Interaction mode to fly over a surface.

Arguments:
  • move_step: float, optional The step size for moving the camera in the plane of the surface.
  • angle_step: float, optional The step size for rotating the camera.
Example:

interaction_modes3.py

camera
211    @property
212    def camera(self):
213        """Return the active camera."""
214        return self.renderer.GetActiveCamera()

Return the active camera.

position
216    @property
217    def position(self):
218        """Return the camera position."""
219        return np.array(self.camera.GetPosition())

Return the camera position.

focal_point
228    @property
229    def focal_point(self):
230        """Return the camera focal point."""
231        return np.array(self.camera.GetFocalPoint())

Return the camera focal point.

class BlenderStyle(vtkmodules.vtkInteractionStyle.vtkInteractorStyleUser):
 423class BlenderStyle(vtki.vtkInteractorStyleUser):
 424    """
 425    Create an interaction style using the Blender default key-bindings.
 426
 427    Camera action code is largely a translation of
 428    [this](https://github.com/Kitware/VTK/blob/master/Interaction/Style/vtkInteractorStyleTrackballCamera.cxx)
 429    Rubber band code
 430    [here](https://gitlab.kitware.com/updega2/vtk/-/blob/d324b2e898b0da080edee76159c2f92e6f71abe2/Rendering/vtkInteractorStyleRubberBandZoom.cxx)
 431
 432    Interaction:
 433        - Left button: select
 434        - Left button drag: rubber band select or line select, depends on the dragged distance
 435
 436        - Middle button         : rotate
 437        - Middle button + shift : pan
 438        - Middle button + ctrl  : zoom
 439        - Middle button + alt   : center view on picked point
 440        - Middle button + alt   : zoom rubber band
 441
 442        - Mouse wheel : zoom
 443
 444        - Right key click: reserved for context-menu
 445
 446
 447    Keys:
 448        - 2 or 3 : toggle perspective view
 449        - a      : zoom all
 450        - x,y,z  : view direction (toggles positive and negative)
 451        - left/right arrows: rotate 45 deg clockwise/ccw about z-axis, snaps to nearest 45 deg
 452        - b      : box zoom
 453        - m      : mouse middle lock (toggles)
 454        - space  : same as middle mouse button
 455        - g      : grab (move actors)
 456        - enter  : accept drag
 457        - esc    : cancel drag, call callbackEscape
 458
 459
 460    Laptop Mode
 461    -----------
 462    Use space or `m` as replacement for middle button (`m` is sticky, space is not),
 463    if `callback_any_key` is assigned then this function is called on every key press.
 464    If this function returns True then further processing of events is stopped.
 465
 466
 467    Moving Actors
 468    -------------
 469    Actors can be moved interactively by the user.
 470    To support custom groups of actors to be moved as a whole the following system
 471    is implemented:
 472
 473    When 'g' is pressed (grab) then a list of all the actors that are being dragged.
 474    By default this is the selection, but this may be altered.
 475    Drag is accepted using enter, click, or g. Drag is cancelled by esc
 476
 477    Events
 478    ------
 479    - `callback_start_drag` is called when initializing the drag.
 480    This is when to assign actors and other data to draginfo.
 481    - `callback_end_drag` is called when the drag is accepted.
 482
 483    Responding to other events
 484    --------------------------
 485    `callback_camera_direction_changed` : executed when camera has rotated but before re-rendering
 486
 487    Example:
 488        [interaction_modes2.py](https://github.com/marcomusy/vedo/blob/master/examples/basic/interaction_modes2.py)
 489
 490    .. note::
 491        This class is based on R. de Bruin's
 492        [DAVE](https://github.com/RubendeBruin/DAVE/blob/master/src/DAVE/visual_helpers/vtkBlenderLikeInteractionStyle.py)
 493        implementation as discussed [in this issue](https://github.com/marcomusy/vedo/discussions/788).
 494    """
 495
 496    def __init__(self):
 497
 498        super().__init__()
 499
 500        self.interactor = None
 501        self.renderer = None
 502
 503        # callback_select is called whenever one or mode props are selected.
 504        # callback will be called with a list of props of which the first entry
 505        # is prop closest to the camera.
 506        self.callback_select = None
 507        self.callback_start_drag = None
 508        self.callback_end_drag = None
 509        self.callback_escape_key = None
 510        self.callback_delete_key = None
 511        self.callback_focus_key = None
 512        self.callback_any_key = None
 513        self.callback_measure = None  # callback with argument float (meters)
 514        self.callback_camera_direction_changed = None
 515
 516        # active drag
 517        # assigned to a _BlenderStyleDragInfo object when dragging is active
 518        self.draginfo: Optional[_BlenderStyleDragInfo] = None
 519
 520        # picking
 521        self.picked_props = []  # will be filled by latest pick
 522
 523        # settings
 524        self.mouse_motion_factor = 20
 525        self.mouse_wheel_motion_factor = 0.1
 526        self.zoom_motion_factor = 0.25
 527
 528        # internals
 529        self.start_x = 0  # start of a drag
 530        self.start_y = 0
 531        self.end_x = 0
 532        self.end_y = 0
 533
 534        self.middle_mouse_lock = False
 535        self.middle_mouse_lock_actor = None  # will be created when required
 536
 537        # Special Modes
 538        self._is_box_zooming = False
 539
 540        # holds an image of the renderer output at the start of a drawing event
 541        self._pixel_array = vtki.vtkUnsignedCharArray()
 542
 543        self._upside_down = False
 544
 545        self._left_button_down = False
 546        self._middle_button_down = False
 547
 548        self.AddObserver("RightButtonPressEvent", self.right_button_press)
 549        self.AddObserver("RightButtonReleaseEvent", self.right_button_release)
 550        self.AddObserver("MiddleButtonPressEvent", self.middle_button_press)
 551        self.AddObserver("MiddleButtonReleaseEvent", self.middle_button_release)
 552        self.AddObserver("MouseWheelForwardEvent", self.mouse_wheel_forward)
 553        self.AddObserver("MouseWheelBackwardEvent", self.mouse_wheel_backward)
 554        self.AddObserver("LeftButtonPressEvent", self.left_button_press)
 555        self.AddObserver("LeftButtonReleaseEvent", self.left_button_release)
 556        self.AddObserver("MouseMoveEvent", self.mouse_move)
 557        self.AddObserver("WindowResizeEvent", self.window_resized)
 558        # ^does not seem to fire!
 559        self.AddObserver("KeyPressEvent", self.key_press)
 560        self.AddObserver("KeyReleaseEvent", self.key_release)
 561
 562    def right_button_press(self, _obj, _event):
 563        pass
 564
 565    def right_button_release(self, _obj, _event):
 566        pass
 567
 568    def middle_button_press(self, _obj, _event):
 569        self._middle_button_down = True
 570
 571    def middle_button_release(self, _obj, _event):
 572        self._middle_button_down = False
 573
 574        # perform middle button focus event if ALT is down
 575        if self.GetInteractor().GetAltKey():
 576            # print("Middle button released while ALT is down")
 577            # try to pick an object at the current mouse position
 578            rwi = self.GetInteractor()
 579            self.start_x, self.start_y = rwi.GetEventPosition()
 580            props = self.perform_picking_on_selection()
 581
 582            if props:
 583                self.focus_on(props[0])
 584
 585    def mouse_wheel_backward(self, _obj, _event):
 586        self.move_mouse_wheel(-1)
 587
 588    def mouse_wheel_forward(self, _obj, _event):
 589        self.move_mouse_wheel(1)
 590
 591    def mouse_move(self, _obj, _event):
 592        interactor = self.GetInteractor()
 593
 594        # Find the renderer that is active below the current mouse position
 595        x, y = interactor.GetEventPosition()
 596        self.FindPokedRenderer(x, y)
 597
 598        Shift = interactor.GetShiftKey()
 599        Ctrl = interactor.GetControlKey()
 600        Alt = interactor.GetAltKey()
 601
 602        MiddleButton = self._middle_button_down or self.middle_mouse_lock
 603
 604        # start with the special modes
 605        if self._is_box_zooming:
 606            self.draw_dragged_selection()
 607        elif MiddleButton and not Shift and not Ctrl and not Alt:
 608            self.rotate()
 609        elif MiddleButton and Shift and not Ctrl and not Alt:
 610            self.pan()
 611        elif MiddleButton and Ctrl and not Shift and not Alt:
 612            self.zoom()  # Dolly
 613        elif self.draginfo is not None:
 614            self.execute_drag()
 615        elif self._left_button_down and Ctrl and Shift:
 616            self.draw_measurement()
 617        elif self._left_button_down:
 618            self.draw_dragged_selection()
 619
 620        self.InvokeEvent("InteractionEvent", None)
 621
 622    def move_mouse_wheel(self, direction):
 623        rwi = self.GetInteractor()
 624
 625        # Find the renderer that is active below the current mouse position
 626        x, y = rwi.GetEventPosition()
 627        self.FindPokedRenderer(x, y)
 628
 629        # The movement
 630        ren = self.GetCurrentRenderer()
 631
 632        #   // Calculate the focal depth since we'll be using it a lot
 633        camera = ren.GetActiveCamera()
 634        viewFocus = camera.GetFocalPoint()
 635
 636        temp_out = [0, 0, 0]
 637        self.ComputeWorldToDisplay(
 638            ren, viewFocus[0], viewFocus[1], viewFocus[2], temp_out
 639        )
 640        focalDepth = temp_out[2]
 641
 642        newPickPoint = [0, 0, 0, 0]
 643        x, y = rwi.GetEventPosition()
 644        self.ComputeDisplayToWorld(ren, x, y, focalDepth, newPickPoint)
 645
 646        # Has to recalc old mouse point since the viewport has moved,
 647        # so can't move it outside the loop
 648        oldPickPoint = [0, 0, 0, 0]
 649        # xp, yp = rwi.GetLastEventPosition()
 650
 651        # find the center of the window
 652        size = rwi.GetRenderWindow().GetSize()
 653        xp = size[0] / 2
 654        yp = size[1] / 2
 655
 656        self.ComputeDisplayToWorld(ren, xp, yp, focalDepth, oldPickPoint)
 657        # Camera motion is reversed
 658        move_factor = -1 * self.zoom_motion_factor * direction
 659
 660        motionVector = (
 661            move_factor * (oldPickPoint[0] - newPickPoint[0]),
 662            move_factor * (oldPickPoint[1] - newPickPoint[1]),
 663            move_factor * (oldPickPoint[2] - newPickPoint[2]),
 664        )
 665
 666        viewFocus = camera.GetFocalPoint()  # do we need to do this again? Already did this
 667        viewPoint = camera.GetPosition()
 668
 669        camera.SetFocalPoint(
 670            motionVector[0] + viewFocus[0],
 671            motionVector[1] + viewFocus[1],
 672            motionVector[2] + viewFocus[2],
 673        )
 674        camera.SetPosition(
 675            motionVector[0] + viewPoint[0],
 676            motionVector[1] + viewPoint[1],
 677            motionVector[2] + viewPoint[2],
 678        )
 679
 680        # the zooming
 681        factor = self.mouse_motion_factor * self.mouse_wheel_motion_factor
 682        self.zoom_by_step(direction * factor)
 683
 684    def zoom_by_step(self, step):
 685        if self.GetCurrentRenderer():
 686            self.StartDolly()
 687            self.dolly(pow(1.1, step))
 688            self.EndDolly()
 689
 690    def left_button_press(self, _obj, _event):
 691        if self._is_box_zooming:
 692            return
 693        if self.draginfo:
 694            return
 695
 696        self._left_button_down = True
 697
 698        interactor = self.GetInteractor()
 699        Shift = interactor.GetShiftKey()
 700        Ctrl = interactor.GetControlKey()
 701
 702        if Shift and Ctrl:
 703            if not self.GetCurrentRenderer().GetActiveCamera().GetParallelProjection():
 704                self.toggle_parallel_projection()
 705
 706        rwi = self.GetInteractor()
 707        self.start_x, self.start_y = rwi.GetEventPosition()
 708        self.end_x = self.start_x
 709        self.end_y = self.start_y
 710
 711        self.initialize_screen_drawing()
 712
 713    def left_button_release(self, _obj, _event):
 714        # print("LeftButtonRelease")
 715        if self._is_box_zooming:
 716            self._is_box_zooming = False
 717            self.zoom_box(self.start_x, self.start_y, self.end_x, self.end_y)
 718            return
 719
 720        if self.draginfo:
 721            self.finish_drag()
 722            return
 723
 724        self._left_button_down = False
 725
 726        interactor = self.GetInteractor()
 727
 728        Shift = interactor.GetShiftKey()
 729        Ctrl = interactor.GetControlKey()
 730        # Alt = interactor.GetAltKey()
 731
 732        if Ctrl and Shift:
 733            pass  # we were drawing the measurement
 734
 735        else:
 736            if self.callback_select:
 737                props = self.perform_picking_on_selection()
 738                if props:  # only call back if anything was selected
 739                    self.picked_props = tuple(props)
 740                    self.callback_select(props)
 741
 742        # remove the selection rubber band / line
 743        self.GetInteractor().Render()
 744
 745    def key_press(self, obj, _event):
 746
 747        key = obj.GetKeySym()
 748        KEY = key.upper()
 749
 750        # logging.info(f"Key Press: {key}")
 751        if self.callback_any_key:
 752            if self.callback_any_key(key):
 753                return
 754
 755        if KEY == "M":
 756            self.middle_mouse_lock = not self.middle_mouse_lock
 757            self._update_middle_mouse_button_lock_actor()
 758        elif KEY == "G":
 759            if self.draginfo is not None:
 760                self.finish_drag()
 761            else:
 762                if self.callback_start_drag:
 763                    self.callback_start_drag()
 764                else:
 765                    self.start_drag()
 766                    # internally calls end-drag if drag is already active
 767        elif KEY == "ESCAPE":
 768            if self.callback_escape_key:
 769                self.callback_escape_key()
 770            if self.draginfo is not None:
 771                self.cancel_drag()
 772        elif KEY == "DELETE":
 773            if self.callback_delete_key:
 774                self.callback_delete_key()
 775        elif KEY == "RETURN":
 776            if self.draginfo:
 777                self.finish_drag()
 778        elif KEY == "SPACE":
 779            self.middle_mouse_lock = True
 780            # self._update_middle_mouse_button_lock_actor()
 781            # self.GrabFocus("MouseMoveEvent", self)
 782            # # TODO: grab and release focus; possible from python?
 783        elif KEY == "B":
 784            self._is_box_zooming = True
 785            rwi = self.GetInteractor()
 786            self.start_x, self.start_y = rwi.GetEventPosition()
 787            self.end_x = self.start_x
 788            self.end_y = self.start_y
 789            self.initialize_screen_drawing()
 790        elif KEY in ("2", "3"):
 791            self.toggle_parallel_projection()
 792        elif KEY == "A":
 793            self.zoom_fit()
 794        elif KEY == "X":
 795            self.set_view_x()
 796        elif KEY == "Y":
 797            self.set_view_y()
 798        elif KEY == "Z":
 799            self.set_view_z()
 800        elif KEY == "LEFT":
 801            self.rotate_discrete_step(1)
 802        elif KEY == "RIGHT":
 803            self.rotate_discrete_step(-1)
 804        elif KEY == "UP":
 805            self.rotate_turtable_by(0, 10)
 806        elif KEY == "DOWN":
 807            self.rotate_turtable_by(0, -10)
 808        elif KEY == "PLUS":
 809            self.zoom_by_step(2)
 810        elif KEY == "MINUS":
 811            self.zoom_by_step(-2)
 812        elif KEY == "F":
 813            if self.callback_focus_key:
 814                self.callback_focus_key()
 815
 816        self.InvokeEvent("InteractionEvent", None)
 817
 818    def key_release(self, obj, _event):
 819        key = obj.GetKeySym()
 820        KEY = key.upper()
 821        # print(f"Key release: {key}")
 822        if KEY == "SPACE":
 823            if self.middle_mouse_lock:
 824                self.middle_mouse_lock = False
 825                self._update_middle_mouse_button_lock_actor()
 826
 827    def window_resized(self):
 828        # print("window resized")
 829        self.initialize_screen_drawing()
 830
 831    def rotate_discrete_step(self, movement_direction, step=22.5):
 832        """
 833        Rotates CW or CCW to the nearest 45 deg angle
 834        - includes some fuzzyness to determine about which axis
 835        """
 836        camera = self.GetCurrentRenderer().GetActiveCamera()
 837
 838        step = np.deg2rad(step)
 839
 840        direction = -np.array(camera.GetViewPlaneNormal())  # current camera direction
 841
 842        if abs(direction[2]) < 0.7:
 843            # horizontal view, rotate camera position about Z-axis
 844            angle = np.arctan2(direction[1], direction[0])
 845
 846            # find the nearest angle that is an integer number of steps
 847            if movement_direction > 0:
 848                angle = step * np.floor((angle + 0.1 * step) / step) + step
 849            else:
 850                angle = -step * np.floor(-(angle - 0.1 * step) / step) - step
 851
 852            dist = np.linalg.norm(direction[:2])
 853            direction[0] = np.cos(angle) * dist
 854            direction[1] = np.sin(angle) * dist
 855
 856            self.set_camera_direction(direction)
 857
 858        else:  # Top or bottom like view - rotate camera "up" direction
 859
 860            up = np.array(camera.GetViewUp())
 861            angle = np.arctan2(up[1], up[0])
 862
 863            # find the nearest angle that is an integer number of steps
 864            if movement_direction > 0:
 865                angle = step * np.floor((angle + 0.1 * step) / step) + step
 866            else:
 867                angle = -step * np.floor(-(angle - 0.1 * step) / step) - step
 868
 869            dist = np.linalg.norm(up[:2])
 870            up[0] = np.cos(angle) * dist
 871            up[1] = np.sin(angle) * dist
 872
 873            camera.SetViewUp(up)
 874            camera.OrthogonalizeViewUp()
 875            self.GetInteractor().Render()
 876
 877    def toggle_parallel_projection(self):
 878        renderer = self.GetCurrentRenderer()
 879        camera = renderer.GetActiveCamera()
 880        camera.SetParallelProjection(not bool(camera.GetParallelProjection()))
 881        self.GetInteractor().Render()
 882
 883    def set_view_x(self):
 884        self.set_camera_plane_direction((1, 0, 0))
 885
 886    def set_view_y(self):
 887        self.set_camera_plane_direction((0, 1, 0))
 888
 889    def set_view_z(self):
 890        self.set_camera_plane_direction((0, 0, 1))
 891
 892    def zoom_fit(self):
 893        self.GetCurrentRenderer().ResetCamera()
 894        self.GetInteractor().Render()
 895
 896    def set_camera_plane_direction(self, direction):
 897        """
 898        Sets the camera to display a plane of which direction is the normal
 899        - includes logic to reverse the direction if benificial
 900        """
 901        camera = self.GetCurrentRenderer().GetActiveCamera()
 902
 903        direction = np.array(direction)
 904        normal = camera.GetViewPlaneNormal()
 905        # can not set the normal, need to change the position to do that
 906
 907        current_alignment = np.dot(normal, -direction)
 908        if abs(current_alignment) > 0.9999:
 909            # print("toggling")
 910            direction = -np.array(normal)
 911        elif current_alignment > 0:  # find the nearest plane
 912            # print("reversing to find nearest")
 913            direction = -direction
 914
 915        self.set_camera_direction(-direction)
 916
 917    def set_camera_direction(self, direction):
 918        """Sets the camera to this direction, sets view up if horizontal enough"""
 919        direction = np.array(direction)
 920
 921        ren = self.GetCurrentRenderer()
 922        camera = ren.GetActiveCamera()
 923        rwi = self.GetInteractor()
 924
 925        pos = np.array(camera.GetPosition())
 926        focal = np.array(camera.GetFocalPoint())
 927        dist = np.linalg.norm(pos - focal)
 928
 929        pos = focal - dist * direction
 930        camera.SetPosition(pos)
 931
 932        if abs(direction[2]) < 0.9:
 933            camera.SetViewUp(0, 0, 1)
 934        elif direction[2] > 0.9:
 935            camera.SetViewUp(0, -1, 0)
 936        else:
 937            camera.SetViewUp(0, 1, 0)
 938
 939        camera.OrthogonalizeViewUp()
 940
 941        if self.GetAutoAdjustCameraClippingRange():
 942            ren.ResetCameraClippingRange()
 943
 944        if rwi.GetLightFollowCamera():
 945            ren.UpdateLightsGeometryToFollowCamera()
 946
 947        if self.callback_camera_direction_changed:
 948            self.callback_camera_direction_changed()
 949
 950        self.GetInteractor().Render()
 951
 952    def perform_picking_on_selection(self):
 953        """
 954        Performs 3d picking on the current dragged selection
 955
 956        If the distance between the start and endpoints is less than the threshold
 957        then a SINGLE 3d prop is picked along the line.
 958
 959        The selection area is drawn by the rubber band and is defined by
 960        `self.start_x, self.start_y, self.end_x, self.end_y`
 961        """
 962        renderer = self.GetCurrentRenderer()
 963        if not renderer:
 964            return []
 965
 966        assemblyPath = renderer.PickProp(self.start_x, self.start_y, self.end_x, self.end_y)
 967
 968        # re-pick in larger area if nothing is returned
 969        if not assemblyPath:
 970            self.start_x -= 2
 971            self.end_x += 2
 972            self.start_y -= 2
 973            self.end_y += 2
 974            assemblyPath = renderer.PickProp(self.start_x, self.start_y, self.end_x, self.end_y)
 975
 976        # The nearest prop (by Z-value)
 977        if assemblyPath:
 978            assert (
 979                assemblyPath.GetNumberOfItems() == 1
 980            ), "Wrong assumption on number of returned nodes when picking"
 981            nearest_prop = assemblyPath.GetItemAsObject(0).GetViewProp()
 982
 983            # all props
 984            collection = renderer.GetPickResultProps()
 985            props = [collection.GetItemAsObject(i) for i in range(collection.GetNumberOfItems())]
 986
 987            props.remove(nearest_prop)
 988            props.insert(0, nearest_prop)
 989            return props
 990
 991        else:
 992            return []
 993
 994    # ----------- actor dragging ------------
 995    def start_drag(self):
 996        """Starts the drag operation"""
 997        if self.callback_start_drag:
 998            # print("Calling callback_start_drag")
 999            self.callback_start_drag()
1000            return
1001        else:  # grab the current selection
1002            if self.picked_props:
1003                self.start_drag_on_props(self.picked_props)
1004            else:
1005                pass
1006                # print('Can not start drag,
1007                # nothing selected and callback_start_drag not assigned')
1008
1009    def finish_drag(self):
1010        """Finishes the drag operation"""
1011        # print('Finished drag')
1012        if self.callback_end_drag:
1013            # reset actor positions as actors positions will be controlled
1014            # by called functions
1015            for pos0, actor in zip(
1016                self.draginfo.dragged_actors_original_positions,
1017                self.draginfo.actors_dragging,
1018            ):
1019                actor.SetPosition(pos0)
1020            self.callback_end_drag(self.draginfo)
1021
1022        self.draginfo = None
1023
1024    def start_drag_on_props(self, props):
1025        """
1026        Starts drag on the provided props (actors) by filling self.draginfo"""
1027        if self.draginfo is not None:
1028            self.finish_drag()
1029            return
1030
1031        # create and fill drag-info
1032        draginfo = _BlenderStyleDragInfo()
1033        draginfo.actors_dragging = props  # [*actors, *outlines]
1034
1035        for a in draginfo.actors_dragging:
1036            draginfo.dragged_actors_original_positions.append(a.GetPosition())
1037
1038        # Get the start position of the drag in 3d
1039        rwi = self.GetInteractor()
1040        ren = self.GetCurrentRenderer()
1041        camera = ren.GetActiveCamera()
1042        viewFocus = camera.GetFocalPoint()
1043
1044        temp_out = [0, 0, 0]
1045        self.ComputeWorldToDisplay(
1046            ren, viewFocus[0], viewFocus[1], viewFocus[2], temp_out
1047        )
1048        focalDepth = temp_out[2]
1049
1050        newPickPoint = [0, 0, 0, 0]
1051        x, y = rwi.GetEventPosition()
1052        self.ComputeDisplayToWorld(ren, x, y, focalDepth, newPickPoint)
1053
1054        mouse_pos_3d = np.array(newPickPoint[:3])
1055        draginfo.start_position_3d = mouse_pos_3d
1056        self.draginfo = draginfo
1057
1058    def execute_drag(self):
1059        """Executes the drag operation"""
1060        rwi = self.GetInteractor()
1061        ren = self.GetCurrentRenderer()
1062
1063        camera = ren.GetActiveCamera()
1064        viewFocus = camera.GetFocalPoint()
1065
1066        # Get the picked point in 3d
1067        temp_out = [0, 0, 0]
1068        self.ComputeWorldToDisplay(
1069            ren, viewFocus[0], viewFocus[1], viewFocus[2], temp_out
1070        )
1071        focalDepth = temp_out[2]
1072
1073        newPickPoint = [0, 0, 0, 0]
1074        x, y = rwi.GetEventPosition()
1075        self.ComputeDisplayToWorld(ren, x, y, focalDepth, newPickPoint)
1076
1077        mouse_pos_3d = np.array(newPickPoint[:3])
1078
1079        # compute the delta and execute
1080
1081        delta = np.array(mouse_pos_3d) - self.draginfo.start_position_3d
1082        # print(f'Delta = {delta}')
1083        view_normal = np.array(ren.GetActiveCamera().GetViewPlaneNormal())
1084
1085        delta_inplane = delta - view_normal * np.dot(delta, view_normal)
1086        # print(f'delta_inplane = {delta_inplane}')
1087
1088        for pos0, actor in zip(
1089            self.draginfo.dragged_actors_original_positions,
1090            self.draginfo.actors_dragging,
1091        ):
1092            m = actor.GetUserMatrix()
1093            if m:
1094                print("UserMatrices/transforms not supported")
1095                # m.Invert() #inplace
1096                # rotated = m.MultiplyFloatPoint([*delta_inplane, 1])
1097                # actor.SetPosition(pos0 + np.array(rotated[:3]))
1098            actor.SetPosition(pos0 + delta_inplane)
1099
1100        # print(f'Set position to {pos0 + delta_inplane}')
1101        self.draginfo.delta = delta_inplane  # store the current delta
1102
1103        self.GetInteractor().Render()
1104
1105    def cancel_drag(self):
1106        """Cancels the drag and restored the original positions of all dragged actors"""
1107        for pos0, actor in zip(
1108            self.draginfo.dragged_actors_original_positions,
1109            self.draginfo.actors_dragging,
1110        ):
1111            actor.SetPosition(pos0)
1112        self.draginfo = None
1113        self.GetInteractor().Render()
1114
1115    # ----------- end dragging --------------
1116
1117    def zoom(self):
1118        """Zooms the camera"""
1119        rwi = self.GetInteractor()
1120        _, y = rwi.GetEventPosition()
1121        _, yp = rwi.GetLastEventPosition()
1122        direction = y - yp
1123        self.move_mouse_wheel(direction / 10)
1124
1125    def pan(self):
1126        """Pans the camera"""
1127        ren = self.GetCurrentRenderer()
1128
1129        if ren:
1130            rwi = self.GetInteractor()
1131
1132            #   // Calculate the focal depth since we'll be using it a lot
1133            camera = ren.GetActiveCamera()
1134            viewFocus = camera.GetFocalPoint()
1135
1136            temp_out = [0, 0, 0]
1137            self.ComputeWorldToDisplay(
1138                ren, viewFocus[0], viewFocus[1], viewFocus[2], temp_out
1139            )
1140            focalDepth = temp_out[2]
1141
1142            newPickPoint = [0, 0, 0, 0]
1143            x, y = rwi.GetEventPosition()
1144            self.ComputeDisplayToWorld(ren, x, y, focalDepth, newPickPoint)
1145
1146            # Has to recalc old mouse point since the viewport has moved,
1147            # so can't move it outside the loop
1148
1149            oldPickPoint = [0, 0, 0, 0]
1150            xp, yp = rwi.GetLastEventPosition()
1151            self.ComputeDisplayToWorld(ren, xp, yp, focalDepth, oldPickPoint)
1152            #
1153            # Camera motion is reversed
1154            motionVector = (
1155                oldPickPoint[0] - newPickPoint[0],
1156                oldPickPoint[1] - newPickPoint[1],
1157                oldPickPoint[2] - newPickPoint[2],
1158            )
1159
1160            viewFocus = camera.GetFocalPoint()  # do we need to do this again? Already did this
1161            viewPoint = camera.GetPosition()
1162
1163            camera.SetFocalPoint(
1164                motionVector[0] + viewFocus[0],
1165                motionVector[1] + viewFocus[1],
1166                motionVector[2] + viewFocus[2],
1167            )
1168            camera.SetPosition(
1169                motionVector[0] + viewPoint[0],
1170                motionVector[1] + viewPoint[1],
1171                motionVector[2] + viewPoint[2],
1172            )
1173
1174            if rwi.GetLightFollowCamera():
1175                ren.UpdateLightsGeometryToFollowCamera()
1176
1177            self.GetInteractor().Render()
1178
1179    def rotate(self):
1180        """Rotates the camera"""
1181        ren = self.GetCurrentRenderer()
1182
1183        if ren:
1184
1185            rwi = self.GetInteractor()
1186            dx = rwi.GetEventPosition()[0] - rwi.GetLastEventPosition()[0]
1187            dy = rwi.GetEventPosition()[1] - rwi.GetLastEventPosition()[1]
1188
1189            size = ren.GetRenderWindow().GetSize()
1190            delta_elevation = -20.0 / size[1]
1191            delta_azimuth = -20.0 / size[0]
1192
1193            rxf = dx * delta_azimuth * self.mouse_motion_factor
1194            ryf = dy * delta_elevation * self.mouse_motion_factor
1195
1196            self.rotate_turtable_by(rxf, ryf)
1197
1198    def rotate_turtable_by(self, rxf, ryf):
1199        """Rotates the camera in turn-table mode"""
1200        ren = self.GetCurrentRenderer()
1201        rwi = self.GetInteractor()
1202
1203        # rfx is rotation about the global Z vector (turn-table mode)
1204        # rfy is rotation about the side vector
1205
1206        camera = ren.GetActiveCamera()
1207        campos = np.array(camera.GetPosition())
1208        focal = np.array(camera.GetFocalPoint())
1209        up = camera.GetViewUp()
1210        upside_down_factor = -1 if up[2] < 0 else 1
1211
1212        # rotate about focal point
1213        P = campos - focal  # camera position
1214
1215        # Rotate left/right about the global Z axis
1216        H = np.linalg.norm(P[:2])  # horizontal distance of camera to focal point
1217        elev = np.arctan2(P[2], H)  # elevation
1218
1219        # if the camera is near the poles, then derive the azimuth from the up-vector
1220        sin_elev = np.sin(elev)
1221        if abs(sin_elev) < 0.8:
1222            azi = np.arctan2(P[1], P[0])  # azimuth from camera position
1223        else:
1224            if sin_elev < -0.8:
1225                azi = np.arctan2(upside_down_factor * up[1], upside_down_factor * up[0])
1226            else:
1227                azi = np.arctan2(-upside_down_factor * up[1], -upside_down_factor * up[0])
1228
1229        D = np.linalg.norm(P)  # distance from focal point to camera
1230
1231        # apply the change in azimuth and elevation
1232        azi_new = azi + rxf / 60
1233        elev_new = elev + upside_down_factor * ryf / 60
1234
1235        # the changed elevation changes H (D stays the same)
1236        Hnew = D * np.cos(elev_new)
1237
1238        # calculate new camera position relative to focal point
1239        Pnew = np.array((Hnew * np.cos(azi_new), Hnew * np.sin(azi_new), D * np.sin(elev_new)))
1240
1241        # calculate the up-direction of the camera
1242        up_z = upside_down_factor * np.cos(elev_new)  # z follows directly from elevation
1243        up_h = upside_down_factor * np.sin(elev_new)  # horizontal component
1244        #
1245        # if upside_down:
1246        #     up_z = -up_z
1247        #     up_h = -up_h
1248        up = (-up_h * np.cos(azi_new), -up_h * np.sin(azi_new), up_z)
1249        new_pos = focal + Pnew
1250        camera.SetViewUp(up)
1251        camera.SetPosition(new_pos)
1252        camera.OrthogonalizeViewUp()
1253
1254        # Update
1255
1256        if self.GetAutoAdjustCameraClippingRange():
1257            ren.ResetCameraClippingRange()
1258
1259        if rwi.GetLightFollowCamera():
1260            ren.UpdateLightsGeometryToFollowCamera()
1261
1262        if self.callback_camera_direction_changed:
1263            self.callback_camera_direction_changed()
1264
1265        self.GetInteractor().Render()
1266
1267    def zoom_box(self, x1, y1, x2, y2):
1268        """Zooms to a box"""
1269        if x1 > x2:
1270            #swap x1 and x2
1271            x1, x2 = x2, x1
1272        if y1 > y2:
1273            y1, y2 = y2, y1
1274
1275        width = x2 - x1
1276        height = y2 - y1
1277
1278        ren = self.GetCurrentRenderer()
1279        size = ren.GetSize()
1280        origin = ren.GetOrigin()
1281        camera = ren.GetActiveCamera()
1282
1283        # Assuming we're drawing the band on the view-plane
1284        rbcenter = (x1 + width / 2, y1 + height / 2, 0)
1285
1286        ren.SetDisplayPoint(rbcenter)
1287        ren.DisplayToView()
1288        ren.ViewToWorld()
1289
1290        worldRBCenter = ren.GetWorldPoint()
1291
1292        invw = 1.0 / worldRBCenter[3]
1293        worldRBCenter = [c * invw for c in worldRBCenter]
1294        winCenter = [origin[0] + 0.5 * size[0], origin[1] + 0.5 * size[1], 0]
1295
1296        ren.SetDisplayPoint(winCenter)
1297        ren.DisplayToView()
1298        ren.ViewToWorld()
1299
1300        worldWinCenter = ren.GetWorldPoint()
1301        invw = 1.0 / worldWinCenter[3]
1302        worldWinCenter = [c * invw for c in worldWinCenter]
1303
1304        translation = [
1305            worldRBCenter[0] - worldWinCenter[0],
1306            worldRBCenter[1] - worldWinCenter[1],
1307            worldRBCenter[2] - worldWinCenter[2],
1308        ]
1309
1310        pos = camera.GetPosition()
1311        fp = camera.GetFocalPoint()
1312        #
1313        pos = [pos[i] + translation[i] for i in range(3)]
1314        fp = [fp[i] + translation[i] for i in range(3)]
1315
1316        #
1317        camera.SetPosition(pos)
1318        camera.SetFocalPoint(fp)
1319
1320        if width > height:
1321            if width:
1322                camera.Zoom(size[0] / width)
1323        else:
1324            if height:
1325                camera.Zoom(size[1] / height)
1326
1327        self.GetInteractor().Render()
1328
1329    def focus_on(self, prop3D):
1330        """Move the camera to focus on this particular prop3D"""
1331
1332        position = prop3D.GetPosition()
1333
1334        ren = self.GetCurrentRenderer()
1335        camera = ren.GetActiveCamera()
1336
1337        fp = camera.GetFocalPoint()
1338        pos = camera.GetPosition()
1339
1340        camera.SetFocalPoint(position)
1341        camera.SetPosition(
1342            position[0] - fp[0] + pos[0],
1343            position[1] - fp[1] + pos[1],
1344            position[2] - fp[2] + pos[2],
1345        )
1346
1347        if self.GetAutoAdjustCameraClippingRange():
1348            ren.ResetCameraClippingRange()
1349
1350        rwi = self.GetInteractor()
1351        if rwi.GetLightFollowCamera():
1352            ren.UpdateLightsGeometryToFollowCamera()
1353
1354        self.GetInteractor().Render()
1355
1356    def dolly(self, factor):
1357        """Dolly the camera"""
1358        ren = self.GetCurrentRenderer()
1359
1360        if ren:
1361            camera = ren.GetActiveCamera()
1362
1363            if camera.GetParallelProjection():
1364                camera.SetParallelScale(camera.GetParallelScale() / factor)
1365            else:
1366                camera.Dolly(factor)
1367                if self.GetAutoAdjustCameraClippingRange():
1368                    ren.ResetCameraClippingRange()
1369
1370            # if not do_not_update:
1371            #     rwi = self.GetInteractor()
1372            #     if rwi.GetLightFollowCamera():
1373            #         ren.UpdateLightsGeometryToFollowCamera()
1374            #     # rwi.Render()
1375
1376    def draw_measurement(self):
1377        """Draws a measurement line"""
1378        rwi = self.GetInteractor()
1379        self.end_x, self.end_y = rwi.GetEventPosition()
1380        self.draw_line(self.start_x, self.end_x, self.start_y, self.end_y)
1381
1382    def draw_dragged_selection(self):
1383        """Draws the selection rubber band"""
1384        rwi = self.GetInteractor()
1385        self.end_x, self.end_y = rwi.GetEventPosition()
1386        self.draw_rubber_band(self.start_x, self.end_x, self.start_y, self.end_y)
1387
1388    def initialize_screen_drawing(self):
1389        """Initializes the screen drawing"""
1390        # make an image of the currently rendered image
1391
1392        rwi = self.GetInteractor()
1393        rwin = rwi.GetRenderWindow()
1394
1395        size = rwin.GetSize()
1396
1397        self._pixel_array.Initialize()
1398        self._pixel_array.SetNumberOfComponents(4)
1399        self._pixel_array.SetNumberOfTuples(size[0] * size[1])
1400
1401        front = 1  # what does this do?
1402        rwin.GetRGBACharPixelData(0, 0, size[0] - 1, size[1] - 1, front, self._pixel_array)
1403
1404    def draw_rubber_band(self, x1, x2, y1, y2):
1405        """Draws a rubber band"""
1406        rwi = self.GetInteractor()
1407        rwin = rwi.GetRenderWindow()
1408
1409        size = rwin.GetSize()
1410
1411        tempPA = vtki.vtkUnsignedCharArray()
1412        tempPA.DeepCopy(self._pixel_array)
1413
1414        # check size, viewport may have been resized in the mean-time
1415        if tempPA.GetNumberOfTuples() != size[0] * size[1]:
1416            # print(
1417            #     "Starting new screen-image - viewport has resized without us knowing"
1418            # )
1419            self.initialize_screen_drawing()
1420            self.draw_rubber_band(x1, x2, y1, y2)
1421            return
1422
1423        x2 = min(x2, size[0] - 1)
1424        y2 = min(y2, size[1] - 1)
1425
1426        x2 = max(x2, 0)
1427        y2 = max(y2, 0)
1428
1429        # Modify the pixel array
1430        width = abs(x2 - x1)
1431        height = abs(y2 - y1)
1432        minx = min(x2, x1)
1433        miny = min(y2, y1)
1434
1435        # draw top and bottom
1436        for i in range(width):
1437
1438            # c = round((10*i % 254)/254) * 254  # find some alternating color
1439            c = 0
1440
1441            idx = (miny * size[0]) + minx + i
1442            tempPA.SetTuple(idx, (c, c, c, 1))
1443
1444            idx = ((miny + height) * size[0]) + minx + i
1445            tempPA.SetTuple(idx, (c, c, c, 1))
1446
1447        # draw left and right
1448        for i in range(height):
1449            # c = round((10 * i % 254) / 254) * 254  # find some alternating color
1450            c = 0
1451
1452            idx = ((miny + i) * size[0]) + minx
1453            tempPA.SetTuple(idx, (c, c, c, 1))
1454
1455            idx = idx + width
1456            tempPA.SetTuple(idx, (c, c, c, 1))
1457
1458        # and Copy back to the window
1459        rwin.SetRGBACharPixelData(0, 0, size[0] - 1, size[1] - 1, tempPA, 0)
1460        rwin.Frame()
1461
1462    def line2pixels(self, x1, x2, y1, y2):
1463        """Returns the x and y values of the pixels on a line between x1,y1 and x2,y2.
1464        If start and end are identical then a single point is returned"""
1465
1466        dx = x2 - x1
1467        dy = y2 - y1
1468
1469        if dx == 0 and dy == 0:
1470            return [x1], [y1]
1471
1472        if abs(dx) > abs(dy):
1473            dhdw = dy / dx
1474            r = range(0, dx, int(dx / abs(dx)))
1475            x = [x1 + i for i in r]
1476            y = [round(y1 + dhdw * i) for i in r]
1477        else:
1478            dwdh = dx / dy
1479            r = range(0, dy, int(dy / abs(dy)))
1480            y = [y1 + i for i in r]
1481            x = [round(x1 + i * dwdh) for i in r]
1482
1483        return x, y
1484
1485    def draw_line(self, x1, x2, y1, y2):
1486        rwi = self.GetInteractor()
1487        rwin = rwi.GetRenderWindow()
1488
1489        size = rwin.GetSize()
1490
1491        x1 = min(max(x1, 0), size[0])
1492        x2 = min(max(x2, 0), size[0])
1493        y1 = min(max(y1, 0), size[1])
1494        y2 = min(max(y2, 0), size[1])
1495
1496        tempPA = vtki.vtkUnsignedCharArray()
1497        tempPA.DeepCopy(self._pixel_array)
1498
1499        xs, ys = self.line2pixels(x1, x2, y1, y2)
1500        for x, y in zip(xs, ys):
1501            idx = (y * size[0]) + x
1502            tempPA.SetTuple(idx, (0, 0, 0, 1))
1503
1504        # and Copy back to the window
1505        rwin.SetRGBACharPixelData(0, 0, size[0] - 1, size[1] - 1, tempPA, 0)
1506
1507        camera = self.GetCurrentRenderer().GetActiveCamera()
1508        scale = camera.GetParallelScale()
1509
1510        # Set/Get the scaling used for a parallel projection, i.e.
1511        #
1512        # the half of the height of the viewport in world-coordinate distances.
1513        # The default is 1. Note that the "scale" parameter works as an "inverse scale"
1514        #  larger numbers produce smaller images.
1515        # This method has no effect in perspective projection mode
1516
1517        half_height = size[1] / 2
1518        # half_height [px] = scale [world-coordinates]
1519
1520        length = ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5
1521        meters_per_pixel = scale / half_height
1522        meters = length * meters_per_pixel
1523
1524        if camera.GetParallelProjection():
1525            print(f"Line length = {length} px = {meters} m")
1526        else:
1527            print("Need to be in non-perspective mode to measure. Press 2 or 3 to get there")
1528
1529        if self.callback_measure:
1530            self.callback_measure(meters)
1531
1532        # # can we add something to the window here?
1533        # freeType = vtk.FreeTypeTools.GetInstance()
1534        # textProperty = vtki.vtkTextProperty()
1535        # textProperty.SetJustificationToLeft()
1536        # textProperty.SetFontSize(24)
1537        # textProperty.SetOrientation(25)
1538        #
1539        # textImage = vtki.vtkImageData()
1540        # freeType.RenderString(textProperty, "a somewhat longer text", 72, textImage)
1541        # # this does not give an error, assume it works
1542        # #
1543        # textImage.GetDimensions()
1544        # textImage.GetExtent()
1545        #
1546        # # # Now put the textImage in the RenderWindow
1547        # rwin.SetRGBACharPixelData(0, 0, size[0] - 1, size[1] - 1, textImage, 0)
1548
1549        rwin.Frame()
1550
1551    def _update_middle_mouse_button_lock_actor(self):
1552
1553        if self.middle_mouse_lock_actor is None:
1554            # create the actor
1555            # Create a text on the top-rightcenter
1556            textMapper = vtki.new("TextMapper")
1557            textMapper.SetInput("Middle mouse lock [m or space] active")
1558            textProp = textMapper.GetTextProperty()
1559            textProp.SetFontSize(12)
1560            textProp.SetFontFamilyToTimes()
1561            textProp.BoldOff()
1562            textProp.ItalicOff()
1563            textProp.ShadowOff()
1564            textProp.SetVerticalJustificationToTop()
1565            textProp.SetJustificationToCentered()
1566            textProp.SetColor((0, 0, 0))
1567
1568            self.middle_mouse_lock_actor = vtki.vtkActor2D()
1569            self.middle_mouse_lock_actor.SetMapper(textMapper)
1570            self.middle_mouse_lock_actor.GetPositionCoordinate().SetCoordinateSystemToNormalizedDisplay()
1571            self.middle_mouse_lock_actor.GetPositionCoordinate().SetValue(0.5, 0.98)
1572
1573            self.GetCurrentRenderer().AddActor(self.middle_mouse_lock_actor)
1574
1575        self.middle_mouse_lock_actor.SetVisibility(self.middle_mouse_lock)
1576        self.GetInteractor().Render()

Create an interaction style using the Blender default key-bindings.

Camera action code is largely a translation of this Rubber band code here

Interaction:
  • Left button: select
  • Left button drag: rubber band select or line select, depends on the dragged distance

  • Middle button : rotate

  • Middle button + shift : pan
  • Middle button + ctrl : zoom
  • Middle button + alt : center view on picked point
  • Middle button + alt : zoom rubber band

  • Mouse wheel : zoom

  • Right key click: reserved for context-menu

Keys:
  • 2 or 3 : toggle perspective view
  • a : zoom all
  • x,y,z : view direction (toggles positive and negative)
  • left/right arrows: rotate 45 deg clockwise/ccw about z-axis, snaps to nearest 45 deg
  • b : box zoom
  • m : mouse middle lock (toggles)
  • space : same as middle mouse button
  • g : grab (move actors)
  • enter : accept drag
  • esc : cancel drag, call callbackEscape

Laptop Mode

Use space or m as replacement for middle button (m is sticky, space is not), if callback_any_key is assigned then this function is called on every key press. If this function returns True then further processing of events is stopped.

Moving Actors

Actors can be moved interactively by the user. To support custom groups of actors to be moved as a whole the following system is implemented:

When 'g' is pressed (grab) then a list of all the actors that are being dragged. By default this is the selection, but this may be altered. Drag is accepted using enter, click, or g. Drag is cancelled by esc

Events

  • callback_start_drag is called when initializing the drag. This is when to assign actors and other data to draginfo.
  • callback_end_drag is called when the drag is accepted.

Responding to other events

callback_camera_direction_changed : executed when camera has rotated but before re-rendering

Example:

interaction_modes2.py

This class is based on R. de Bruin's DAVE implementation as discussed in this issue.

BlenderStyle()
496    def __init__(self):
497
498        super().__init__()
499
500        self.interactor = None
501        self.renderer = None
502
503        # callback_select is called whenever one or mode props are selected.
504        # callback will be called with a list of props of which the first entry
505        # is prop closest to the camera.
506        self.callback_select = None
507        self.callback_start_drag = None
508        self.callback_end_drag = None
509        self.callback_escape_key = None
510        self.callback_delete_key = None
511        self.callback_focus_key = None
512        self.callback_any_key = None
513        self.callback_measure = None  # callback with argument float (meters)
514        self.callback_camera_direction_changed = None
515
516        # active drag
517        # assigned to a _BlenderStyleDragInfo object when dragging is active
518        self.draginfo: Optional[_BlenderStyleDragInfo] = None
519
520        # picking
521        self.picked_props = []  # will be filled by latest pick
522
523        # settings
524        self.mouse_motion_factor = 20
525        self.mouse_wheel_motion_factor = 0.1
526        self.zoom_motion_factor = 0.25
527
528        # internals
529        self.start_x = 0  # start of a drag
530        self.start_y = 0
531        self.end_x = 0
532        self.end_y = 0
533
534        self.middle_mouse_lock = False
535        self.middle_mouse_lock_actor = None  # will be created when required
536
537        # Special Modes
538        self._is_box_zooming = False
539
540        # holds an image of the renderer output at the start of a drawing event
541        self._pixel_array = vtki.vtkUnsignedCharArray()
542
543        self._upside_down = False
544
545        self._left_button_down = False
546        self._middle_button_down = False
547
548        self.AddObserver("RightButtonPressEvent", self.right_button_press)
549        self.AddObserver("RightButtonReleaseEvent", self.right_button_release)
550        self.AddObserver("MiddleButtonPressEvent", self.middle_button_press)
551        self.AddObserver("MiddleButtonReleaseEvent", self.middle_button_release)
552        self.AddObserver("MouseWheelForwardEvent", self.mouse_wheel_forward)
553        self.AddObserver("MouseWheelBackwardEvent", self.mouse_wheel_backward)
554        self.AddObserver("LeftButtonPressEvent", self.left_button_press)
555        self.AddObserver("LeftButtonReleaseEvent", self.left_button_release)
556        self.AddObserver("MouseMoveEvent", self.mouse_move)
557        self.AddObserver("WindowResizeEvent", self.window_resized)
558        # ^does not seem to fire!
559        self.AddObserver("KeyPressEvent", self.key_press)
560        self.AddObserver("KeyReleaseEvent", self.key_release)
def right_button_press(self, _obj, _event):
562    def right_button_press(self, _obj, _event):
563        pass
def right_button_release(self, _obj, _event):
565    def right_button_release(self, _obj, _event):
566        pass
def middle_button_press(self, _obj, _event):
568    def middle_button_press(self, _obj, _event):
569        self._middle_button_down = True
def middle_button_release(self, _obj, _event):
571    def middle_button_release(self, _obj, _event):
572        self._middle_button_down = False
573
574        # perform middle button focus event if ALT is down
575        if self.GetInteractor().GetAltKey():
576            # print("Middle button released while ALT is down")
577            # try to pick an object at the current mouse position
578            rwi = self.GetInteractor()
579            self.start_x, self.start_y = rwi.GetEventPosition()
580            props = self.perform_picking_on_selection()
581
582            if props:
583                self.focus_on(props[0])
def mouse_wheel_backward(self, _obj, _event):
585    def mouse_wheel_backward(self, _obj, _event):
586        self.move_mouse_wheel(-1)
def mouse_wheel_forward(self, _obj, _event):
588    def mouse_wheel_forward(self, _obj, _event):
589        self.move_mouse_wheel(1)
def mouse_move(self, _obj, _event):
591    def mouse_move(self, _obj, _event):
592        interactor = self.GetInteractor()
593
594        # Find the renderer that is active below the current mouse position
595        x, y = interactor.GetEventPosition()
596        self.FindPokedRenderer(x, y)
597
598        Shift = interactor.GetShiftKey()
599        Ctrl = interactor.GetControlKey()
600        Alt = interactor.GetAltKey()
601
602        MiddleButton = self._middle_button_down or self.middle_mouse_lock
603
604        # start with the special modes
605        if self._is_box_zooming:
606            self.draw_dragged_selection()
607        elif MiddleButton and not Shift and not Ctrl and not Alt:
608            self.rotate()
609        elif MiddleButton and Shift and not Ctrl and not Alt:
610            self.pan()
611        elif MiddleButton and Ctrl and not Shift and not Alt:
612            self.zoom()  # Dolly
613        elif self.draginfo is not None:
614            self.execute_drag()
615        elif self._left_button_down and Ctrl and Shift:
616            self.draw_measurement()
617        elif self._left_button_down:
618            self.draw_dragged_selection()
619
620        self.InvokeEvent("InteractionEvent", None)
def move_mouse_wheel(self, direction):
622    def move_mouse_wheel(self, direction):
623        rwi = self.GetInteractor()
624
625        # Find the renderer that is active below the current mouse position
626        x, y = rwi.GetEventPosition()
627        self.FindPokedRenderer(x, y)
628
629        # The movement
630        ren = self.GetCurrentRenderer()
631
632        #   // Calculate the focal depth since we'll be using it a lot
633        camera = ren.GetActiveCamera()
634        viewFocus = camera.GetFocalPoint()
635
636        temp_out = [0, 0, 0]
637        self.ComputeWorldToDisplay(
638            ren, viewFocus[0], viewFocus[1], viewFocus[2], temp_out
639        )
640        focalDepth = temp_out[2]
641
642        newPickPoint = [0, 0, 0, 0]
643        x, y = rwi.GetEventPosition()
644        self.ComputeDisplayToWorld(ren, x, y, focalDepth, newPickPoint)
645
646        # Has to recalc old mouse point since the viewport has moved,
647        # so can't move it outside the loop
648        oldPickPoint = [0, 0, 0, 0]
649        # xp, yp = rwi.GetLastEventPosition()
650
651        # find the center of the window
652        size = rwi.GetRenderWindow().GetSize()
653        xp = size[0] / 2
654        yp = size[1] / 2
655
656        self.ComputeDisplayToWorld(ren, xp, yp, focalDepth, oldPickPoint)
657        # Camera motion is reversed
658        move_factor = -1 * self.zoom_motion_factor * direction
659
660        motionVector = (
661            move_factor * (oldPickPoint[0] - newPickPoint[0]),
662            move_factor * (oldPickPoint[1] - newPickPoint[1]),
663            move_factor * (oldPickPoint[2] - newPickPoint[2]),
664        )
665
666        viewFocus = camera.GetFocalPoint()  # do we need to do this again? Already did this
667        viewPoint = camera.GetPosition()
668
669        camera.SetFocalPoint(
670            motionVector[0] + viewFocus[0],
671            motionVector[1] + viewFocus[1],
672            motionVector[2] + viewFocus[2],
673        )
674        camera.SetPosition(
675            motionVector[0] + viewPoint[0],
676            motionVector[1] + viewPoint[1],
677            motionVector[2] + viewPoint[2],
678        )
679
680        # the zooming
681        factor = self.mouse_motion_factor * self.mouse_wheel_motion_factor
682        self.zoom_by_step(direction * factor)
def zoom_by_step(self, step):
684    def zoom_by_step(self, step):
685        if self.GetCurrentRenderer():
686            self.StartDolly()
687            self.dolly(pow(1.1, step))
688            self.EndDolly()
def left_button_press(self, _obj, _event):
690    def left_button_press(self, _obj, _event):
691        if self._is_box_zooming:
692            return
693        if self.draginfo:
694            return
695
696        self._left_button_down = True
697
698        interactor = self.GetInteractor()
699        Shift = interactor.GetShiftKey()
700        Ctrl = interactor.GetControlKey()
701
702        if Shift and Ctrl:
703            if not self.GetCurrentRenderer().GetActiveCamera().GetParallelProjection():
704                self.toggle_parallel_projection()
705
706        rwi = self.GetInteractor()
707        self.start_x, self.start_y = rwi.GetEventPosition()
708        self.end_x = self.start_x
709        self.end_y = self.start_y
710
711        self.initialize_screen_drawing()
def left_button_release(self, _obj, _event):
713    def left_button_release(self, _obj, _event):
714        # print("LeftButtonRelease")
715        if self._is_box_zooming:
716            self._is_box_zooming = False
717            self.zoom_box(self.start_x, self.start_y, self.end_x, self.end_y)
718            return
719
720        if self.draginfo:
721            self.finish_drag()
722            return
723
724        self._left_button_down = False
725
726        interactor = self.GetInteractor()
727
728        Shift = interactor.GetShiftKey()
729        Ctrl = interactor.GetControlKey()
730        # Alt = interactor.GetAltKey()
731
732        if Ctrl and Shift:
733            pass  # we were drawing the measurement
734
735        else:
736            if self.callback_select:
737                props = self.perform_picking_on_selection()
738                if props:  # only call back if anything was selected
739                    self.picked_props = tuple(props)
740                    self.callback_select(props)
741
742        # remove the selection rubber band / line
743        self.GetInteractor().Render()
def key_press(self, obj, _event):
745    def key_press(self, obj, _event):
746
747        key = obj.GetKeySym()
748        KEY = key.upper()
749
750        # logging.info(f"Key Press: {key}")
751        if self.callback_any_key:
752            if self.callback_any_key(key):
753                return
754
755        if KEY == "M":
756            self.middle_mouse_lock = not self.middle_mouse_lock
757            self._update_middle_mouse_button_lock_actor()
758        elif KEY == "G":
759            if self.draginfo is not None:
760                self.finish_drag()
761            else:
762                if self.callback_start_drag:
763                    self.callback_start_drag()
764                else:
765                    self.start_drag()
766                    # internally calls end-drag if drag is already active
767        elif KEY == "ESCAPE":
768            if self.callback_escape_key:
769                self.callback_escape_key()
770            if self.draginfo is not None:
771                self.cancel_drag()
772        elif KEY == "DELETE":
773            if self.callback_delete_key:
774                self.callback_delete_key()
775        elif KEY == "RETURN":
776            if self.draginfo:
777                self.finish_drag()
778        elif KEY == "SPACE":
779            self.middle_mouse_lock = True
780            # self._update_middle_mouse_button_lock_actor()
781            # self.GrabFocus("MouseMoveEvent", self)
782            # # TODO: grab and release focus; possible from python?
783        elif KEY == "B":
784            self._is_box_zooming = True
785            rwi = self.GetInteractor()
786            self.start_x, self.start_y = rwi.GetEventPosition()
787            self.end_x = self.start_x
788            self.end_y = self.start_y
789            self.initialize_screen_drawing()
790        elif KEY in ("2", "3"):
791            self.toggle_parallel_projection()
792        elif KEY == "A":
793            self.zoom_fit()
794        elif KEY == "X":
795            self.set_view_x()
796        elif KEY == "Y":
797            self.set_view_y()
798        elif KEY == "Z":
799            self.set_view_z()
800        elif KEY == "LEFT":
801            self.rotate_discrete_step(1)
802        elif KEY == "RIGHT":
803            self.rotate_discrete_step(-1)
804        elif KEY == "UP":
805            self.rotate_turtable_by(0, 10)
806        elif KEY == "DOWN":
807            self.rotate_turtable_by(0, -10)
808        elif KEY == "PLUS":
809            self.zoom_by_step(2)
810        elif KEY == "MINUS":
811            self.zoom_by_step(-2)
812        elif KEY == "F":
813            if self.callback_focus_key:
814                self.callback_focus_key()
815
816        self.InvokeEvent("InteractionEvent", None)
def key_release(self, obj, _event):
818    def key_release(self, obj, _event):
819        key = obj.GetKeySym()
820        KEY = key.upper()
821        # print(f"Key release: {key}")
822        if KEY == "SPACE":
823            if self.middle_mouse_lock:
824                self.middle_mouse_lock = False
825                self._update_middle_mouse_button_lock_actor()
def window_resized(self):
827    def window_resized(self):
828        # print("window resized")
829        self.initialize_screen_drawing()
def rotate_discrete_step(self, movement_direction, step=22.5):
831    def rotate_discrete_step(self, movement_direction, step=22.5):
832        """
833        Rotates CW or CCW to the nearest 45 deg angle
834        - includes some fuzzyness to determine about which axis
835        """
836        camera = self.GetCurrentRenderer().GetActiveCamera()
837
838        step = np.deg2rad(step)
839
840        direction = -np.array(camera.GetViewPlaneNormal())  # current camera direction
841
842        if abs(direction[2]) < 0.7:
843            # horizontal view, rotate camera position about Z-axis
844            angle = np.arctan2(direction[1], direction[0])
845
846            # find the nearest angle that is an integer number of steps
847            if movement_direction > 0:
848                angle = step * np.floor((angle + 0.1 * step) / step) + step
849            else:
850                angle = -step * np.floor(-(angle - 0.1 * step) / step) - step
851
852            dist = np.linalg.norm(direction[:2])
853            direction[0] = np.cos(angle) * dist
854            direction[1] = np.sin(angle) * dist
855
856            self.set_camera_direction(direction)
857
858        else:  # Top or bottom like view - rotate camera "up" direction
859
860            up = np.array(camera.GetViewUp())
861            angle = np.arctan2(up[1], up[0])
862
863            # find the nearest angle that is an integer number of steps
864            if movement_direction > 0:
865                angle = step * np.floor((angle + 0.1 * step) / step) + step
866            else:
867                angle = -step * np.floor(-(angle - 0.1 * step) / step) - step
868
869            dist = np.linalg.norm(up[:2])
870            up[0] = np.cos(angle) * dist
871            up[1] = np.sin(angle) * dist
872
873            camera.SetViewUp(up)
874            camera.OrthogonalizeViewUp()
875            self.GetInteractor().Render()

Rotates CW or CCW to the nearest 45 deg angle

  • includes some fuzzyness to determine about which axis
def toggle_parallel_projection(self):
877    def toggle_parallel_projection(self):
878        renderer = self.GetCurrentRenderer()
879        camera = renderer.GetActiveCamera()
880        camera.SetParallelProjection(not bool(camera.GetParallelProjection()))
881        self.GetInteractor().Render()
def set_view_x(self):
883    def set_view_x(self):
884        self.set_camera_plane_direction((1, 0, 0))
def set_view_y(self):
886    def set_view_y(self):
887        self.set_camera_plane_direction((0, 1, 0))
def set_view_z(self):
889    def set_view_z(self):
890        self.set_camera_plane_direction((0, 0, 1))
def zoom_fit(self):
892    def zoom_fit(self):
893        self.GetCurrentRenderer().ResetCamera()
894        self.GetInteractor().Render()
def set_camera_plane_direction(self, direction):
896    def set_camera_plane_direction(self, direction):
897        """
898        Sets the camera to display a plane of which direction is the normal
899        - includes logic to reverse the direction if benificial
900        """
901        camera = self.GetCurrentRenderer().GetActiveCamera()
902
903        direction = np.array(direction)
904        normal = camera.GetViewPlaneNormal()
905        # can not set the normal, need to change the position to do that
906
907        current_alignment = np.dot(normal, -direction)
908        if abs(current_alignment) > 0.9999:
909            # print("toggling")
910            direction = -np.array(normal)
911        elif current_alignment > 0:  # find the nearest plane
912            # print("reversing to find nearest")
913            direction = -direction
914
915        self.set_camera_direction(-direction)

Sets the camera to display a plane of which direction is the normal

  • includes logic to reverse the direction if benificial
def set_camera_direction(self, direction):
917    def set_camera_direction(self, direction):
918        """Sets the camera to this direction, sets view up if horizontal enough"""
919        direction = np.array(direction)
920
921        ren = self.GetCurrentRenderer()
922        camera = ren.GetActiveCamera()
923        rwi = self.GetInteractor()
924
925        pos = np.array(camera.GetPosition())
926        focal = np.array(camera.GetFocalPoint())
927        dist = np.linalg.norm(pos - focal)
928
929        pos = focal - dist * direction
930        camera.SetPosition(pos)
931
932        if abs(direction[2]) < 0.9:
933            camera.SetViewUp(0, 0, 1)
934        elif direction[2] > 0.9:
935            camera.SetViewUp(0, -1, 0)
936        else:
937            camera.SetViewUp(0, 1, 0)
938
939        camera.OrthogonalizeViewUp()
940
941        if self.GetAutoAdjustCameraClippingRange():
942            ren.ResetCameraClippingRange()
943
944        if rwi.GetLightFollowCamera():
945            ren.UpdateLightsGeometryToFollowCamera()
946
947        if self.callback_camera_direction_changed:
948            self.callback_camera_direction_changed()
949
950        self.GetInteractor().Render()

Sets the camera to this direction, sets view up if horizontal enough

def perform_picking_on_selection(self):
952    def perform_picking_on_selection(self):
953        """
954        Performs 3d picking on the current dragged selection
955
956        If the distance between the start and endpoints is less than the threshold
957        then a SINGLE 3d prop is picked along the line.
958
959        The selection area is drawn by the rubber band and is defined by
960        `self.start_x, self.start_y, self.end_x, self.end_y`
961        """
962        renderer = self.GetCurrentRenderer()
963        if not renderer:
964            return []
965
966        assemblyPath = renderer.PickProp(self.start_x, self.start_y, self.end_x, self.end_y)
967
968        # re-pick in larger area if nothing is returned
969        if not assemblyPath:
970            self.start_x -= 2
971            self.end_x += 2
972            self.start_y -= 2
973            self.end_y += 2
974            assemblyPath = renderer.PickProp(self.start_x, self.start_y, self.end_x, self.end_y)
975
976        # The nearest prop (by Z-value)
977        if assemblyPath:
978            assert (
979                assemblyPath.GetNumberOfItems() == 1
980            ), "Wrong assumption on number of returned nodes when picking"
981            nearest_prop = assemblyPath.GetItemAsObject(0).GetViewProp()
982
983            # all props
984            collection = renderer.GetPickResultProps()
985            props = [collection.GetItemAsObject(i) for i in range(collection.GetNumberOfItems())]
986
987            props.remove(nearest_prop)
988            props.insert(0, nearest_prop)
989            return props
990
991        else:
992            return []

Performs 3d picking on the current dragged selection

If the distance between the start and endpoints is less than the threshold then a SINGLE 3d prop is picked along the line.

The selection area is drawn by the rubber band and is defined by self.start_x, self.start_y, self.end_x, self.end_y

def start_drag(self):
 995    def start_drag(self):
 996        """Starts the drag operation"""
 997        if self.callback_start_drag:
 998            # print("Calling callback_start_drag")
 999            self.callback_start_drag()
1000            return
1001        else:  # grab the current selection
1002            if self.picked_props:
1003                self.start_drag_on_props(self.picked_props)
1004            else:
1005                pass
1006                # print('Can not start drag,
1007                # nothing selected and callback_start_drag not assigned')

Starts the drag operation

def finish_drag(self):
1009    def finish_drag(self):
1010        """Finishes the drag operation"""
1011        # print('Finished drag')
1012        if self.callback_end_drag:
1013            # reset actor positions as actors positions will be controlled
1014            # by called functions
1015            for pos0, actor in zip(
1016                self.draginfo.dragged_actors_original_positions,
1017                self.draginfo.actors_dragging,
1018            ):
1019                actor.SetPosition(pos0)
1020            self.callback_end_drag(self.draginfo)
1021
1022        self.draginfo = None

Finishes the drag operation

def start_drag_on_props(self, props):
1024    def start_drag_on_props(self, props):
1025        """
1026        Starts drag on the provided props (actors) by filling self.draginfo"""
1027        if self.draginfo is not None:
1028            self.finish_drag()
1029            return
1030
1031        # create and fill drag-info
1032        draginfo = _BlenderStyleDragInfo()
1033        draginfo.actors_dragging = props  # [*actors, *outlines]
1034
1035        for a in draginfo.actors_dragging:
1036            draginfo.dragged_actors_original_positions.append(a.GetPosition())
1037
1038        # Get the start position of the drag in 3d
1039        rwi = self.GetInteractor()
1040        ren = self.GetCurrentRenderer()
1041        camera = ren.GetActiveCamera()
1042        viewFocus = camera.GetFocalPoint()
1043
1044        temp_out = [0, 0, 0]
1045        self.ComputeWorldToDisplay(
1046            ren, viewFocus[0], viewFocus[1], viewFocus[2], temp_out
1047        )
1048        focalDepth = temp_out[2]
1049
1050        newPickPoint = [0, 0, 0, 0]
1051        x, y = rwi.GetEventPosition()
1052        self.ComputeDisplayToWorld(ren, x, y, focalDepth, newPickPoint)
1053
1054        mouse_pos_3d = np.array(newPickPoint[:3])
1055        draginfo.start_position_3d = mouse_pos_3d
1056        self.draginfo = draginfo

Starts drag on the provided props (actors) by filling self.draginfo

def execute_drag(self):
1058    def execute_drag(self):
1059        """Executes the drag operation"""
1060        rwi = self.GetInteractor()
1061        ren = self.GetCurrentRenderer()
1062
1063        camera = ren.GetActiveCamera()
1064        viewFocus = camera.GetFocalPoint()
1065
1066        # Get the picked point in 3d
1067        temp_out = [0, 0, 0]
1068        self.ComputeWorldToDisplay(
1069            ren, viewFocus[0], viewFocus[1], viewFocus[2], temp_out
1070        )
1071        focalDepth = temp_out[2]
1072
1073        newPickPoint = [0, 0, 0, 0]
1074        x, y = rwi.GetEventPosition()
1075        self.ComputeDisplayToWorld(ren, x, y, focalDepth, newPickPoint)
1076
1077        mouse_pos_3d = np.array(newPickPoint[:3])
1078
1079        # compute the delta and execute
1080
1081        delta = np.array(mouse_pos_3d) - self.draginfo.start_position_3d
1082        # print(f'Delta = {delta}')
1083        view_normal = np.array(ren.GetActiveCamera().GetViewPlaneNormal())
1084
1085        delta_inplane = delta - view_normal * np.dot(delta, view_normal)
1086        # print(f'delta_inplane = {delta_inplane}')
1087
1088        for pos0, actor in zip(
1089            self.draginfo.dragged_actors_original_positions,
1090            self.draginfo.actors_dragging,
1091        ):
1092            m = actor.GetUserMatrix()
1093            if m:
1094                print("UserMatrices/transforms not supported")
1095                # m.Invert() #inplace
1096                # rotated = m.MultiplyFloatPoint([*delta_inplane, 1])
1097                # actor.SetPosition(pos0 + np.array(rotated[:3]))
1098            actor.SetPosition(pos0 + delta_inplane)
1099
1100        # print(f'Set position to {pos0 + delta_inplane}')
1101        self.draginfo.delta = delta_inplane  # store the current delta
1102
1103        self.GetInteractor().Render()

Executes the drag operation

def cancel_drag(self):
1105    def cancel_drag(self):
1106        """Cancels the drag and restored the original positions of all dragged actors"""
1107        for pos0, actor in zip(
1108            self.draginfo.dragged_actors_original_positions,
1109            self.draginfo.actors_dragging,
1110        ):
1111            actor.SetPosition(pos0)
1112        self.draginfo = None
1113        self.GetInteractor().Render()

Cancels the drag and restored the original positions of all dragged actors

def zoom(self):
1117    def zoom(self):
1118        """Zooms the camera"""
1119        rwi = self.GetInteractor()
1120        _, y = rwi.GetEventPosition()
1121        _, yp = rwi.GetLastEventPosition()
1122        direction = y - yp
1123        self.move_mouse_wheel(direction / 10)

Zooms the camera

def pan(self):
1125    def pan(self):
1126        """Pans the camera"""
1127        ren = self.GetCurrentRenderer()
1128
1129        if ren:
1130            rwi = self.GetInteractor()
1131
1132            #   // Calculate the focal depth since we'll be using it a lot
1133            camera = ren.GetActiveCamera()
1134            viewFocus = camera.GetFocalPoint()
1135
1136            temp_out = [0, 0, 0]
1137            self.ComputeWorldToDisplay(
1138                ren, viewFocus[0], viewFocus[1], viewFocus[2], temp_out
1139            )
1140            focalDepth = temp_out[2]
1141
1142            newPickPoint = [0, 0, 0, 0]
1143            x, y = rwi.GetEventPosition()
1144            self.ComputeDisplayToWorld(ren, x, y, focalDepth, newPickPoint)
1145
1146            # Has to recalc old mouse point since the viewport has moved,
1147            # so can't move it outside the loop
1148
1149            oldPickPoint = [0, 0, 0, 0]
1150            xp, yp = rwi.GetLastEventPosition()
1151            self.ComputeDisplayToWorld(ren, xp, yp, focalDepth, oldPickPoint)
1152            #
1153            # Camera motion is reversed
1154            motionVector = (
1155                oldPickPoint[0] - newPickPoint[0],
1156                oldPickPoint[1] - newPickPoint[1],
1157                oldPickPoint[2] - newPickPoint[2],
1158            )
1159
1160            viewFocus = camera.GetFocalPoint()  # do we need to do this again? Already did this
1161            viewPoint = camera.GetPosition()
1162
1163            camera.SetFocalPoint(
1164                motionVector[0] + viewFocus[0],
1165                motionVector[1] + viewFocus[1],
1166                motionVector[2] + viewFocus[2],
1167            )
1168            camera.SetPosition(
1169                motionVector[0] + viewPoint[0],
1170                motionVector[1] + viewPoint[1],
1171                motionVector[2] + viewPoint[2],
1172            )
1173
1174            if rwi.GetLightFollowCamera():
1175                ren.UpdateLightsGeometryToFollowCamera()
1176
1177            self.GetInteractor().Render()

Pans the camera

def rotate(self):
1179    def rotate(self):
1180        """Rotates the camera"""
1181        ren = self.GetCurrentRenderer()
1182
1183        if ren:
1184
1185            rwi = self.GetInteractor()
1186            dx = rwi.GetEventPosition()[0] - rwi.GetLastEventPosition()[0]
1187            dy = rwi.GetEventPosition()[1] - rwi.GetLastEventPosition()[1]
1188
1189            size = ren.GetRenderWindow().GetSize()
1190            delta_elevation = -20.0 / size[1]
1191            delta_azimuth = -20.0 / size[0]
1192
1193            rxf = dx * delta_azimuth * self.mouse_motion_factor
1194            ryf = dy * delta_elevation * self.mouse_motion_factor
1195
1196            self.rotate_turtable_by(rxf, ryf)

Rotates the camera

def rotate_turtable_by(self, rxf, ryf):
1198    def rotate_turtable_by(self, rxf, ryf):
1199        """Rotates the camera in turn-table mode"""
1200        ren = self.GetCurrentRenderer()
1201        rwi = self.GetInteractor()
1202
1203        # rfx is rotation about the global Z vector (turn-table mode)
1204        # rfy is rotation about the side vector
1205
1206        camera = ren.GetActiveCamera()
1207        campos = np.array(camera.GetPosition())
1208        focal = np.array(camera.GetFocalPoint())
1209        up = camera.GetViewUp()
1210        upside_down_factor = -1 if up[2] < 0 else 1
1211
1212        # rotate about focal point
1213        P = campos - focal  # camera position
1214
1215        # Rotate left/right about the global Z axis
1216        H = np.linalg.norm(P[:2])  # horizontal distance of camera to focal point
1217        elev = np.arctan2(P[2], H)  # elevation
1218
1219        # if the camera is near the poles, then derive the azimuth from the up-vector
1220        sin_elev = np.sin(elev)
1221        if abs(sin_elev) < 0.8:
1222            azi = np.arctan2(P[1], P[0])  # azimuth from camera position
1223        else:
1224            if sin_elev < -0.8:
1225                azi = np.arctan2(upside_down_factor * up[1], upside_down_factor * up[0])
1226            else:
1227                azi = np.arctan2(-upside_down_factor * up[1], -upside_down_factor * up[0])
1228
1229        D = np.linalg.norm(P)  # distance from focal point to camera
1230
1231        # apply the change in azimuth and elevation
1232        azi_new = azi + rxf / 60
1233        elev_new = elev + upside_down_factor * ryf / 60
1234
1235        # the changed elevation changes H (D stays the same)
1236        Hnew = D * np.cos(elev_new)
1237
1238        # calculate new camera position relative to focal point
1239        Pnew = np.array((Hnew * np.cos(azi_new), Hnew * np.sin(azi_new), D * np.sin(elev_new)))
1240
1241        # calculate the up-direction of the camera
1242        up_z = upside_down_factor * np.cos(elev_new)  # z follows directly from elevation
1243        up_h = upside_down_factor * np.sin(elev_new)  # horizontal component
1244        #
1245        # if upside_down:
1246        #     up_z = -up_z
1247        #     up_h = -up_h
1248        up = (-up_h * np.cos(azi_new), -up_h * np.sin(azi_new), up_z)
1249        new_pos = focal + Pnew
1250        camera.SetViewUp(up)
1251        camera.SetPosition(new_pos)
1252        camera.OrthogonalizeViewUp()
1253
1254        # Update
1255
1256        if self.GetAutoAdjustCameraClippingRange():
1257            ren.ResetCameraClippingRange()
1258
1259        if rwi.GetLightFollowCamera():
1260            ren.UpdateLightsGeometryToFollowCamera()
1261
1262        if self.callback_camera_direction_changed:
1263            self.callback_camera_direction_changed()
1264
1265        self.GetInteractor().Render()

Rotates the camera in turn-table mode

def zoom_box(self, x1, y1, x2, y2):
1267    def zoom_box(self, x1, y1, x2, y2):
1268        """Zooms to a box"""
1269        if x1 > x2:
1270            #swap x1 and x2
1271            x1, x2 = x2, x1
1272        if y1 > y2:
1273            y1, y2 = y2, y1
1274
1275        width = x2 - x1
1276        height = y2 - y1
1277
1278        ren = self.GetCurrentRenderer()
1279        size = ren.GetSize()
1280        origin = ren.GetOrigin()
1281        camera = ren.GetActiveCamera()
1282
1283        # Assuming we're drawing the band on the view-plane
1284        rbcenter = (x1 + width / 2, y1 + height / 2, 0)
1285
1286        ren.SetDisplayPoint(rbcenter)
1287        ren.DisplayToView()
1288        ren.ViewToWorld()
1289
1290        worldRBCenter = ren.GetWorldPoint()
1291
1292        invw = 1.0 / worldRBCenter[3]
1293        worldRBCenter = [c * invw for c in worldRBCenter]
1294        winCenter = [origin[0] + 0.5 * size[0], origin[1] + 0.5 * size[1], 0]
1295
1296        ren.SetDisplayPoint(winCenter)
1297        ren.DisplayToView()
1298        ren.ViewToWorld()
1299
1300        worldWinCenter = ren.GetWorldPoint()
1301        invw = 1.0 / worldWinCenter[3]
1302        worldWinCenter = [c * invw for c in worldWinCenter]
1303
1304        translation = [
1305            worldRBCenter[0] - worldWinCenter[0],
1306            worldRBCenter[1] - worldWinCenter[1],
1307            worldRBCenter[2] - worldWinCenter[2],
1308        ]
1309
1310        pos = camera.GetPosition()
1311        fp = camera.GetFocalPoint()
1312        #
1313        pos = [pos[i] + translation[i] for i in range(3)]
1314        fp = [fp[i] + translation[i] for i in range(3)]
1315
1316        #
1317        camera.SetPosition(pos)
1318        camera.SetFocalPoint(fp)
1319
1320        if width > height:
1321            if width:
1322                camera.Zoom(size[0] / width)
1323        else:
1324            if height:
1325                camera.Zoom(size[1] / height)
1326
1327        self.GetInteractor().Render()

Zooms to a box

def focus_on(self, prop3D):
1329    def focus_on(self, prop3D):
1330        """Move the camera to focus on this particular prop3D"""
1331
1332        position = prop3D.GetPosition()
1333
1334        ren = self.GetCurrentRenderer()
1335        camera = ren.GetActiveCamera()
1336
1337        fp = camera.GetFocalPoint()
1338        pos = camera.GetPosition()
1339
1340        camera.SetFocalPoint(position)
1341        camera.SetPosition(
1342            position[0] - fp[0] + pos[0],
1343            position[1] - fp[1] + pos[1],
1344            position[2] - fp[2] + pos[2],
1345        )
1346
1347        if self.GetAutoAdjustCameraClippingRange():
1348            ren.ResetCameraClippingRange()
1349
1350        rwi = self.GetInteractor()
1351        if rwi.GetLightFollowCamera():
1352            ren.UpdateLightsGeometryToFollowCamera()
1353
1354        self.GetInteractor().Render()

Move the camera to focus on this particular prop3D

def dolly(self, factor):
1356    def dolly(self, factor):
1357        """Dolly the camera"""
1358        ren = self.GetCurrentRenderer()
1359
1360        if ren:
1361            camera = ren.GetActiveCamera()
1362
1363            if camera.GetParallelProjection():
1364                camera.SetParallelScale(camera.GetParallelScale() / factor)
1365            else:
1366                camera.Dolly(factor)
1367                if self.GetAutoAdjustCameraClippingRange():
1368                    ren.ResetCameraClippingRange()
1369
1370            # if not do_not_update:
1371            #     rwi = self.GetInteractor()
1372            #     if rwi.GetLightFollowCamera():
1373            #         ren.UpdateLightsGeometryToFollowCamera()
1374            #     # rwi.Render()

Dolly the camera

def draw_measurement(self):
1376    def draw_measurement(self):
1377        """Draws a measurement line"""
1378        rwi = self.GetInteractor()
1379        self.end_x, self.end_y = rwi.GetEventPosition()
1380        self.draw_line(self.start_x, self.end_x, self.start_y, self.end_y)

Draws a measurement line

def draw_dragged_selection(self):
1382    def draw_dragged_selection(self):
1383        """Draws the selection rubber band"""
1384        rwi = self.GetInteractor()
1385        self.end_x, self.end_y = rwi.GetEventPosition()
1386        self.draw_rubber_band(self.start_x, self.end_x, self.start_y, self.end_y)

Draws the selection rubber band

def initialize_screen_drawing(self):
1388    def initialize_screen_drawing(self):
1389        """Initializes the screen drawing"""
1390        # make an image of the currently rendered image
1391
1392        rwi = self.GetInteractor()
1393        rwin = rwi.GetRenderWindow()
1394
1395        size = rwin.GetSize()
1396
1397        self._pixel_array.Initialize()
1398        self._pixel_array.SetNumberOfComponents(4)
1399        self._pixel_array.SetNumberOfTuples(size[0] * size[1])
1400
1401        front = 1  # what does this do?
1402        rwin.GetRGBACharPixelData(0, 0, size[0] - 1, size[1] - 1, front, self._pixel_array)

Initializes the screen drawing

def draw_rubber_band(self, x1, x2, y1, y2):
1404    def draw_rubber_band(self, x1, x2, y1, y2):
1405        """Draws a rubber band"""
1406        rwi = self.GetInteractor()
1407        rwin = rwi.GetRenderWindow()
1408
1409        size = rwin.GetSize()
1410
1411        tempPA = vtki.vtkUnsignedCharArray()
1412        tempPA.DeepCopy(self._pixel_array)
1413
1414        # check size, viewport may have been resized in the mean-time
1415        if tempPA.GetNumberOfTuples() != size[0] * size[1]:
1416            # print(
1417            #     "Starting new screen-image - viewport has resized without us knowing"
1418            # )
1419            self.initialize_screen_drawing()
1420            self.draw_rubber_band(x1, x2, y1, y2)
1421            return
1422
1423        x2 = min(x2, size[0] - 1)
1424        y2 = min(y2, size[1] - 1)
1425
1426        x2 = max(x2, 0)
1427        y2 = max(y2, 0)
1428
1429        # Modify the pixel array
1430        width = abs(x2 - x1)
1431        height = abs(y2 - y1)
1432        minx = min(x2, x1)
1433        miny = min(y2, y1)
1434
1435        # draw top and bottom
1436        for i in range(width):
1437
1438            # c = round((10*i % 254)/254) * 254  # find some alternating color
1439            c = 0
1440
1441            idx = (miny * size[0]) + minx + i
1442            tempPA.SetTuple(idx, (c, c, c, 1))
1443
1444            idx = ((miny + height) * size[0]) + minx + i
1445            tempPA.SetTuple(idx, (c, c, c, 1))
1446
1447        # draw left and right
1448        for i in range(height):
1449            # c = round((10 * i % 254) / 254) * 254  # find some alternating color
1450            c = 0
1451
1452            idx = ((miny + i) * size[0]) + minx
1453            tempPA.SetTuple(idx, (c, c, c, 1))
1454
1455            idx = idx + width
1456            tempPA.SetTuple(idx, (c, c, c, 1))
1457
1458        # and Copy back to the window
1459        rwin.SetRGBACharPixelData(0, 0, size[0] - 1, size[1] - 1, tempPA, 0)
1460        rwin.Frame()

Draws a rubber band

def line2pixels(self, x1, x2, y1, y2):
1462    def line2pixels(self, x1, x2, y1, y2):
1463        """Returns the x and y values of the pixels on a line between x1,y1 and x2,y2.
1464        If start and end are identical then a single point is returned"""
1465
1466        dx = x2 - x1
1467        dy = y2 - y1
1468
1469        if dx == 0 and dy == 0:
1470            return [x1], [y1]
1471
1472        if abs(dx) > abs(dy):
1473            dhdw = dy / dx
1474            r = range(0, dx, int(dx / abs(dx)))
1475            x = [x1 + i for i in r]
1476            y = [round(y1 + dhdw * i) for i in r]
1477        else:
1478            dwdh = dx / dy
1479            r = range(0, dy, int(dy / abs(dy)))
1480            y = [y1 + i for i in r]
1481            x = [round(x1 + i * dwdh) for i in r]
1482
1483        return x, y

Returns the x and y values of the pixels on a line between x1,y1 and x2,y2. If start and end are identical then a single point is returned

def draw_line(self, x1, x2, y1, y2):
1485    def draw_line(self, x1, x2, y1, y2):
1486        rwi = self.GetInteractor()
1487        rwin = rwi.GetRenderWindow()
1488
1489        size = rwin.GetSize()
1490
1491        x1 = min(max(x1, 0), size[0])
1492        x2 = min(max(x2, 0), size[0])
1493        y1 = min(max(y1, 0), size[1])
1494        y2 = min(max(y2, 0), size[1])
1495
1496        tempPA = vtki.vtkUnsignedCharArray()
1497        tempPA.DeepCopy(self._pixel_array)
1498
1499        xs, ys = self.line2pixels(x1, x2, y1, y2)
1500        for x, y in zip(xs, ys):
1501            idx = (y * size[0]) + x
1502            tempPA.SetTuple(idx, (0, 0, 0, 1))
1503
1504        # and Copy back to the window
1505        rwin.SetRGBACharPixelData(0, 0, size[0] - 1, size[1] - 1, tempPA, 0)
1506
1507        camera = self.GetCurrentRenderer().GetActiveCamera()
1508        scale = camera.GetParallelScale()
1509
1510        # Set/Get the scaling used for a parallel projection, i.e.
1511        #
1512        # the half of the height of the viewport in world-coordinate distances.
1513        # The default is 1. Note that the "scale" parameter works as an "inverse scale"
1514        #  larger numbers produce smaller images.
1515        # This method has no effect in perspective projection mode
1516
1517        half_height = size[1] / 2
1518        # half_height [px] = scale [world-coordinates]
1519
1520        length = ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5
1521        meters_per_pixel = scale / half_height
1522        meters = length * meters_per_pixel
1523
1524        if camera.GetParallelProjection():
1525            print(f"Line length = {length} px = {meters} m")
1526        else:
1527            print("Need to be in non-perspective mode to measure. Press 2 or 3 to get there")
1528
1529        if self.callback_measure:
1530            self.callback_measure(meters)
1531
1532        # # can we add something to the window here?
1533        # freeType = vtk.FreeTypeTools.GetInstance()
1534        # textProperty = vtki.vtkTextProperty()
1535        # textProperty.SetJustificationToLeft()
1536        # textProperty.SetFontSize(24)
1537        # textProperty.SetOrientation(25)
1538        #
1539        # textImage = vtki.vtkImageData()
1540        # freeType.RenderString(textProperty, "a somewhat longer text", 72, textImage)
1541        # # this does not give an error, assume it works
1542        # #
1543        # textImage.GetDimensions()
1544        # textImage.GetExtent()
1545        #
1546        # # # Now put the textImage in the RenderWindow
1547        # rwin.SetRGBACharPixelData(0, 0, size[0] - 1, size[1] - 1, textImage, 0)
1548
1549        rwin.Frame()