vedo.interactor_modes

Submodule to customize interaction modes.

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

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

Left button: select

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

Middle button: Navigation

Middle button: rotate

Middle button + shift : pan

Middle button + ctrl : zoom

Middle button + alt : center view on picked point

OR

Middle button + alt : zoom rubber band

Mouse wheel : zoom

Right button : context

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)

callbacks / overriding keys:

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 _BlenderStyleDragInfo dataclass object is assigned to style to style.draginfo.

_BlenderStyleDragInfo includes 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

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

Example:

interaction_modes2.py

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

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

def perform_picking_on_selection(self):
956    def perform_picking_on_selection(self):
957        """
958        Performs 3d picking on the current dragged selection
959
960        If the distance between the start and endpoints is less than the threshold
961        then a SINGLE 3d prop is picked along the line.
962
963        The selection area is drawn by the rubber band and is defined by
964        `self.start_x, self.start_y, self.end_x, self.end_y`
965        """
966        renderer = self.GetCurrentRenderer()
967        if not renderer:
968            return []
969
970        assemblyPath = renderer.PickProp(self.start_x, self.start_y, self.end_x, self.end_y)
971
972        # re-pick in larger area if nothing is returned
973        if not assemblyPath:
974            self.start_x -= 2
975            self.end_x += 2
976            self.start_y -= 2
977            self.end_y += 2
978            assemblyPath = renderer.PickProp(self.start_x, self.start_y, self.end_x, self.end_y)
979
980        # The nearest prop (by Z-value)
981        if assemblyPath:
982            assert (
983                assemblyPath.GetNumberOfItems() == 1
984            ), "Wrong assumption on number of returned nodes when picking"
985            nearest_prop = assemblyPath.GetItemAsObject(0).GetViewProp()
986
987            # all props
988            collection = renderer.GetPickResultProps()
989            props = [collection.GetItemAsObject(i) for i in range(collection.GetNumberOfItems())]
990
991            props.remove(nearest_prop)
992            props.insert(0, nearest_prop)
993            return props
994
995        else:
996            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):
 999    def start_drag(self):
1000        if self.callback_start_drag:
1001            # print("Calling callback_start_drag")
1002            self.callback_start_drag()
1003            return
1004        else:  # grab the current selection
1005            if self.picked_props:
1006                self.start_drag_on_props(self.picked_props)
1007            else:
1008                pass
1009                # print('Can not start drag,
1010                # nothing selected and callback_start_drag not assigned')
def finish_drag(self):
1012    def finish_drag(self):
1013        # print('Finished drag')
1014        if self.callback_end_drag:
1015            # reset actor positions as actors positions will be controlled
1016            # by called functions
1017            for pos0, actor in zip(
1018                self.draginfo.dragged_actors_original_positions,
1019                self.draginfo.actors_dragging,
1020            ):
1021                actor.SetPosition(pos0)
1022            self.callback_end_drag(self.draginfo)
1023
1024        self.draginfo = None
def start_drag_on_props(self, props):
1026    def start_drag_on_props(self, props):
1027        """
1028        Starts drag on the provided props (actors) by filling self.draginfo"""
1029        if self.draginfo is not None:
1030            self.finish_drag()
1031            return
1032
1033        # create and fill drag-info
1034        draginfo = _BlenderStyleDragInfo()
1035        draginfo.actors_dragging = props  # [*actors, *outlines]
1036
1037        for a in draginfo.actors_dragging:
1038            draginfo.dragged_actors_original_positions.append(a.GetPosition())
1039
1040        # Get the start position of the drag in 3d
1041        rwi = self.GetInteractor()
1042        ren = self.GetCurrentRenderer()
1043        camera = ren.GetActiveCamera()
1044        viewFocus = camera.GetFocalPoint()
1045
1046        temp_out = [0, 0, 0]
1047        self.ComputeWorldToDisplay(
1048            ren, viewFocus[0], viewFocus[1], viewFocus[2], temp_out
1049        )
1050        focalDepth = temp_out[2]
1051
1052        newPickPoint = [0, 0, 0, 0]
1053        x, y = rwi.GetEventPosition()
1054        self.ComputeDisplayToWorld(ren, x, y, focalDepth, newPickPoint)
1055
1056        mouse_pos_3d = np.array(newPickPoint[:3])
1057        draginfo.start_position_3d = mouse_pos_3d
1058        self.draginfo = draginfo

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

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

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

def zoom(self):
1118    def zoom(self):
1119        rwi = self.GetInteractor()
1120        x, y = rwi.GetEventPosition()
1121        xp, yp = rwi.GetLastEventPosition()
1122        direction = y - yp
1123        self.move_mouse_wheel(direction / 10)
def pan(self):
1125    def pan(self):
1126
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()
def rotate(self):
1179    def rotate(self):
1180
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)
def rotate_turtable_by(self, rxf, ryf):
1198    def rotate_turtable_by(self, rxf, ryf):
1199
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()
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            _ = x1
1271            x1 = x2
1272            x2 = _
1273        if y1 > y2:
1274            _ = y1
1275            y1 = y2
1276            y2 = _
1277
1278        width = x2 - x1
1279        height = y2 - y1
1280
1281        ren = self.GetCurrentRenderer()
1282        size = ren.GetSize()
1283        origin = ren.GetOrigin()
1284        camera = ren.GetActiveCamera()
1285
1286        # Assuming we're drawing the band on the view-plane
1287        rbcenter = (x1 + width / 2, y1 + height / 2, 0)
1288
1289        ren.SetDisplayPoint(rbcenter)
1290        ren.DisplayToView()
1291        ren.ViewToWorld()
1292
1293        worldRBCenter = ren.GetWorldPoint()
1294
1295        invw = 1.0 / worldRBCenter[3]
1296        worldRBCenter = [c * invw for c in worldRBCenter]
1297        winCenter = [origin[0] + 0.5 * size[0], origin[1] + 0.5 * size[1], 0]
1298
1299        ren.SetDisplayPoint(winCenter)
1300        ren.DisplayToView()
1301        ren.ViewToWorld()
1302
1303        worldWinCenter = ren.GetWorldPoint()
1304        invw = 1.0 / worldWinCenter[3]
1305        worldWinCenter = [c * invw for c in worldWinCenter]
1306
1307        translation = [
1308            worldRBCenter[0] - worldWinCenter[0],
1309            worldRBCenter[1] - worldWinCenter[1],
1310            worldRBCenter[2] - worldWinCenter[2],
1311        ]
1312
1313        pos = camera.GetPosition()
1314        fp = camera.GetFocalPoint()
1315        #
1316        pos = [pos[i] + translation[i] for i in range(3)]
1317        fp = [fp[i] + translation[i] for i in range(3)]
1318
1319        #
1320        camera.SetPosition(pos)
1321        camera.SetFocalPoint(fp)
1322
1323        if width > height:
1324            if width:
1325                camera.Zoom(size[0] / width)
1326        else:
1327            if height:
1328                camera.Zoom(size[1] / height)
1329
1330        self.GetInteractor().Render()

Zooms to a box

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

Move the camera to focus on this particular prop3D

def dolly(self, factor):
1359    def dolly(self, factor):
1360        ren = self.GetCurrentRenderer()
1361
1362        if ren:
1363            camera = ren.GetActiveCamera()
1364
1365            if camera.GetParallelProjection():
1366                camera.SetParallelScale(camera.GetParallelScale() / factor)
1367            else:
1368                camera.Dolly(factor)
1369                if self.GetAutoAdjustCameraClippingRange():
1370                    ren.ResetCameraClippingRange()
1371
1372            # if not do_not_update:
1373            #     rwi = self.GetInteractor()
1374            #     if rwi.GetLightFollowCamera():
1375            #         ren.UpdateLightsGeometryToFollowCamera()
1376            #     # rwi.Render()
def draw_measurement(self):
1378    def draw_measurement(self):
1379        rwi = self.GetInteractor()
1380        self.end_x, self.end_y = rwi.GetEventPosition()
1381        self.draw_line(self.start_x, self.end_x, self.start_y, self.end_y)
def draw_dragged_selection(self):
1383    def draw_dragged_selection(self):
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)
def initialize_screen_drawing(self):
1388    def initialize_screen_drawing(self):
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)
def draw_rubber_band(self, x1, x2, y1, y2):
1403    def draw_rubber_band(self, x1, x2, y1, y2):
1404        rwi = self.GetInteractor()
1405        rwin = rwi.GetRenderWindow()
1406
1407        size = rwin.GetSize()
1408
1409        tempPA = vtki.vtkUnsignedCharArray()
1410        tempPA.DeepCopy(self._pixel_array)
1411
1412        # check size, viewport may have been resized in the mean-time
1413        if tempPA.GetNumberOfTuples() != size[0] * size[1]:
1414            # print(
1415            #     "Starting new screen-image - viewport has resized without us knowing"
1416            # )
1417            self.initialize_screen_drawing()
1418            self.draw_rubber_band(x1, x2, y1, y2)
1419            return
1420
1421        x2 = min(x2, size[0] - 1)
1422        y2 = min(y2, size[1] - 1)
1423
1424        x2 = max(x2, 0)
1425        y2 = max(y2, 0)
1426
1427        # Modify the pixel array
1428        width = abs(x2 - x1)
1429        height = abs(y2 - y1)
1430        minx = min(x2, x1)
1431        miny = min(y2, y1)
1432
1433        # draw top and bottom
1434        for i in range(width):
1435
1436            # c = round((10*i % 254)/254) * 254  # find some alternating color
1437            c = 0
1438
1439            idx = (miny * size[0]) + minx + i
1440            tempPA.SetTuple(idx, (c, c, c, 1))
1441
1442            idx = ((miny + height) * size[0]) + minx + i
1443            tempPA.SetTuple(idx, (c, c, c, 1))
1444
1445        # draw left and right
1446        for i in range(height):
1447            # c = round((10 * i % 254) / 254) * 254  # find some alternating color
1448            c = 0
1449
1450            idx = ((miny + i) * size[0]) + minx
1451            tempPA.SetTuple(idx, (c, c, c, 1))
1452
1453            idx = idx + width
1454            tempPA.SetTuple(idx, (c, c, c, 1))
1455
1456        # and Copy back to the window
1457        rwin.SetRGBACharPixelData(0, 0, size[0] - 1, size[1] - 1, tempPA, 0)
1458        rwin.Frame()
def line2pixels(self, x1, x2, y1, y2):
1460    def line2pixels(self, x1, x2, y1, y2):
1461        """Returns the x and y values of the pixels on a line between x1,y1 and x2,y2.
1462        If start and end are identical then a single point is returned"""
1463
1464        dx = x2 - x1
1465        dy = y2 - y1
1466
1467        if dx == 0 and dy == 0:
1468            return [x1], [y1]
1469
1470        if abs(dx) > abs(dy):
1471            dhdw = dy / dx
1472            r = range(0, dx, int(dx / abs(dx)))
1473            x = [x1 + i for i in r]
1474            y = [round(y1 + dhdw * i) for i in r]
1475        else:
1476            dwdh = dx / dy
1477            r = range(0, dy, int(dy / abs(dy)))
1478            y = [y1 + i for i in r]
1479            x = [round(x1 + i * dwdh) for i in r]
1480
1481        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):
1483    def draw_line(self, x1, x2, y1, y2):
1484        rwi = self.GetInteractor()
1485        rwin = rwi.GetRenderWindow()
1486
1487        size = rwin.GetSize()
1488
1489        x1 = min(max(x1, 0), size[0])
1490        x2 = min(max(x2, 0), size[0])
1491        y1 = min(max(y1, 0), size[1])
1492        y2 = min(max(y2, 0), size[1])
1493
1494        tempPA = vtki.vtkUnsignedCharArray()
1495        tempPA.DeepCopy(self._pixel_array)
1496
1497        xs, ys = self.line2pixels(x1, x2, y1, y2)
1498        for x, y in zip(xs, ys):
1499            idx = (y * size[0]) + x
1500            tempPA.SetTuple(idx, (0, 0, 0, 1))
1501
1502        # and Copy back to the window
1503        rwin.SetRGBACharPixelData(0, 0, size[0] - 1, size[1] - 1, tempPA, 0)
1504
1505        camera = self.GetCurrentRenderer().GetActiveCamera()
1506        scale = camera.GetParallelScale()
1507
1508        # Set/Get the scaling used for a parallel projection, i.e.
1509        #
1510        # the half of the height of the viewport in world-coordinate distances.
1511        # The default is 1. Note that the "scale" parameter works as an "inverse scale"
1512        #  larger numbers produce smaller images.
1513        # This method has no effect in perspective projection mode
1514
1515        half_height = size[1] / 2
1516        # half_height [px] = scale [world-coordinates]
1517
1518        length = ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5
1519        meters_per_pixel = scale / half_height
1520        meters = length * meters_per_pixel
1521
1522        if camera.GetParallelProjection():
1523            print(f"Line length = {length} px = {meters} m")
1524        else:
1525            print("Need to be in non-perspective mode to measure. Press 2 or 3 to get there")
1526
1527        if self.callback_measure:
1528            self.callback_measure(meters)
1529
1530        # # can we add something to the window here?
1531        # freeType = vtk.FreeTypeTools.GetInstance()
1532        # textProperty = vtki.vtkTextProperty()
1533        # textProperty.SetJustificationToLeft()
1534        # textProperty.SetFontSize(24)
1535        # textProperty.SetOrientation(25)
1536        #
1537        # textImage = vtki.vtkImageData()
1538        # freeType.RenderString(textProperty, "a somewhat longer text", 72, textImage)
1539        # # this does not give an error, assume it works
1540        # #
1541        # textImage.GetDimensions()
1542        # textImage.GetExtent()
1543        #
1544        # # # Now put the textImage in the RenderWindow
1545        # rwin.SetRGBACharPixelData(0, 0, size[0] - 1, size[1] - 1, textImage, 0)
1546
1547        rwin.Frame()