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

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

def perform_picking_on_selection(self):
727    def perform_picking_on_selection(self):
728        """
729        Performs 3d picking on the current dragged selection
730
731        If the distance between the start and endpoints is less than the threshold
732        then a SINGLE 3d prop is picked along the line.
733
734        The selection area is drawn by the rubber band and is defined by
735        `self.start_x, self.start_y, self.end_x, self.end_y`
736        """
737        renderer = self.GetCurrentRenderer()
738        if not renderer:
739            return []
740
741        assemblyPath = renderer.PickProp(self.start_x, self.start_y, self.end_x, self.end_y)
742
743        # re-pick in larger area if nothing is returned
744        if not assemblyPath:
745            self.start_x -= 2
746            self.end_x += 2
747            self.start_y -= 2
748            self.end_y += 2
749            assemblyPath = renderer.PickProp(self.start_x, self.start_y, self.end_x, self.end_y)
750
751        # The nearest prop (by Z-value)
752        if assemblyPath:
753            assert (
754                assemblyPath.GetNumberOfItems() == 1
755            ), "Wrong assumption on number of returned nodes when picking"
756            nearest_prop = assemblyPath.GetItemAsObject(0).GetViewProp()
757
758            # all props
759            collection = renderer.GetPickResultProps()
760            props = [collection.GetItemAsObject(i) for i in range(collection.GetNumberOfItems())]
761
762            props.remove(nearest_prop)
763            props.insert(0, nearest_prop)
764            return props
765
766        else:
767            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):
770    def start_drag(self):
771        if self.callback_start_drag:
772            # print("Calling callback_start_drag")
773            self.callback_start_drag()
774            return
775        else:  # grab the current selection
776            if self.picked_props:
777                self.start_drag_on_props(self.picked_props)
778            else:
779                pass
780                # print('Can not start drag, 
781                # nothing selected and callback_start_drag not assigned')
def finish_drag(self):
783    def finish_drag(self):
784        # print('Finished drag')
785        if self.callback_end_drag:
786            # reset actor positions as actors positions will be controlled
787            # by called functions
788            for pos0, actor in zip(
789                self.draginfo.dragged_actors_original_positions,
790                self.draginfo.actors_dragging
791            ):
792                actor.SetPosition(pos0)
793            self.callback_end_drag(self.draginfo)
794
795        self.draginfo = None
def start_drag_on_props(self, props):
797    def start_drag_on_props(self, props):
798        """
799        Starts drag on the provided props (actors) by filling self.draginfo"""
800        if self.draginfo is not None:
801            self.finish_drag()
802            return
803
804        # create and fill drag-info
805        draginfo = _BlenderStyleDragInfo()
806        draginfo.actors_dragging = props  # [*actors, *outlines]
807
808        for a in draginfo.actors_dragging:
809            draginfo.dragged_actors_original_positions.append(a.GetPosition())  # numpy ndarray
810
811        # Get the start position of the drag in 3d
812        rwi = self.GetInteractor()
813        ren = self.GetCurrentRenderer()
814        camera = ren.GetActiveCamera()
815        viewFocus = camera.GetFocalPoint()
816
817        temp_out = [0, 0, 0]
818        self.ComputeWorldToDisplay(
819            ren, viewFocus[0], viewFocus[1], viewFocus[2],
820            temp_out
821        )
822        focalDepth = temp_out[2]
823
824        newPickPoint = [0, 0, 0, 0]
825        x, y = rwi.GetEventPosition()
826        self.ComputeDisplayToWorld(
827            ren, x, y, focalDepth, newPickPoint)
828
829        mouse_pos_3d = np.array(newPickPoint[:3])
830        draginfo.start_position_3d = mouse_pos_3d
831        self.draginfo = draginfo

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

def execute_drag(self):
833    def execute_drag(self):
834
835        rwi = self.GetInteractor()
836        ren = self.GetCurrentRenderer()
837
838        camera = ren.GetActiveCamera()
839        viewFocus = camera.GetFocalPoint()
840
841        # Get the picked point in 3d
842        temp_out = [0, 0, 0]
843        self.ComputeWorldToDisplay(
844            ren, viewFocus[0], viewFocus[1], viewFocus[2], temp_out
845        )
846        focalDepth = temp_out[2]
847
848        newPickPoint = [0, 0, 0, 0]
849        x, y = rwi.GetEventPosition()
850        self.ComputeDisplayToWorld(ren, x, y, focalDepth, newPickPoint)
851
852        mouse_pos_3d = np.array(newPickPoint[:3])
853
854        # compute the delta and execute
855
856        delta = np.array(mouse_pos_3d) - self.draginfo.start_position_3d
857        # print(f'Delta = {delta}')
858        view_normal = np.array(ren.GetActiveCamera().GetViewPlaneNormal())
859
860        delta_inplane = delta - view_normal * np.dot(delta, view_normal)
861        # print(f'delta_inplane = {delta_inplane}')
862
863        for pos0, actor in zip(
864            self.draginfo.dragged_actors_original_positions, 
865            self.draginfo.actors_dragging
866        ):
867            m = actor.GetUserMatrix()
868            if m:
869                print("UserMatrices/transforms not supported")
870                # m.Invert() #inplace
871                # rotated = m.MultiplyFloatPoint([*delta_inplane, 1])
872                # actor.SetPosition(pos0 + np.array(rotated[:3]))
873            actor.SetPosition(pos0 + delta_inplane)
874
875        # print(f'Set position to {pos0 + delta_inplane}')
876        self.draginfo.delta = delta_inplane  # store the current delta
877
878        self.GetInteractor().Render()
def cancel_drag(self):
880    def cancel_drag(self):
881        """Cancels the drag and restored the original positions of all dragged actors"""
882        for pos0, actor in zip(
883            self.draginfo.dragged_actors_original_positions, 
884            self.draginfo.actors_dragging
885        ):
886            actor.SetPosition(pos0)
887        self.draginfo = None
888        self.GetInteractor().Render()

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

def zoom(self):
892    def zoom(self):
893        rwi = self.GetInteractor()
894        x, y = rwi.GetEventPosition()
895        xp, yp = rwi.GetLastEventPosition()
896
897        direction = y - yp
898        self.move_mouse_wheel(direction / 10)
def pan(self):
900    def pan(self):
901
902        ren = self.GetCurrentRenderer()
903
904        if ren:
905            rwi = self.GetInteractor()
906
907            #   // Calculate the focal depth since we'll be using it a lot
908            camera = ren.GetActiveCamera()
909            viewFocus = camera.GetFocalPoint()
910
911            temp_out = [0, 0, 0]
912            self.ComputeWorldToDisplay(
913                ren, viewFocus[0], viewFocus[1], viewFocus[2], temp_out
914            )
915            focalDepth = temp_out[2]
916
917            newPickPoint = [0, 0, 0, 0]
918            x, y = rwi.GetEventPosition()
919            self.ComputeDisplayToWorld(ren, x, y, focalDepth, newPickPoint)
920
921            #   // Has to recalc old mouse point since the viewport has moved,
922            #   // so can't move it outside the loop
923
924            oldPickPoint = [0, 0, 0, 0]
925            xp, yp = rwi.GetLastEventPosition()
926            self.ComputeDisplayToWorld(ren, xp, yp, focalDepth, oldPickPoint)
927            #
928            #   // Camera motion is reversed
929            #
930            motionVector = (
931                oldPickPoint[0] - newPickPoint[0],
932                oldPickPoint[1] - newPickPoint[1],
933                oldPickPoint[2] - newPickPoint[2],
934            )
935
936            viewFocus = camera.GetFocalPoint()  # do we need to do this again? Already did this
937            viewPoint = camera.GetPosition()
938
939            camera.SetFocalPoint(
940                motionVector[0] + viewFocus[0],
941                motionVector[1] + viewFocus[1],
942                motionVector[2] + viewFocus[2],
943            )
944            camera.SetPosition(
945                motionVector[0] + viewPoint[0],
946                motionVector[1] + viewPoint[1],
947                motionVector[2] + viewPoint[2],
948            )
949
950            if rwi.GetLightFollowCamera():
951                ren.UpdateLightsGeometryToFollowCamera()
952
953            self.GetInteractor().Render()
def rotate(self):
955    def rotate(self):
956
957        ren = self.GetCurrentRenderer()
958
959        if ren:
960
961            rwi = self.GetInteractor()
962            dx = rwi.GetEventPosition()[0] - rwi.GetLastEventPosition()[0]
963            dy = rwi.GetEventPosition()[1] - rwi.GetLastEventPosition()[1]
964
965            size = ren.GetRenderWindow().GetSize()
966            delta_elevation = -20.0 / size[1]
967            delta_azimuth = -20.0 / size[0]
968
969            rxf = dx * delta_azimuth * self.mouse_motion_factor
970            ryf = dy * delta_elevation * self.mouse_motion_factor
971
972            self.rotate_turtable_by(rxf, ryf)
def rotate_turtable_by(self, rxf, ryf):
 974    def rotate_turtable_by(self, rxf, ryf):
 975
 976        ren = self.GetCurrentRenderer()
 977        rwi = self.GetInteractor()
 978
 979        # rfx is rotation about the global Z vector (turn-table mode)
 980        # rfy is rotation about the side vector
 981
 982        camera = ren.GetActiveCamera()
 983        campos = np.array(camera.GetPosition())
 984        focal = np.array(camera.GetFocalPoint())
 985        up = camera.GetViewUp()
 986        upside_down_factor = -1 if up[2] < 0 else 1
 987
 988        # rotate about focal point
 989
 990        P = campos - focal  # camera position
 991
 992        # Rotate left/right about the global Z axis
 993        H = np.linalg.norm(P[:2])  # horizontal distance of camera to focal point
 994        elev = np.arctan2(P[2], H)  # elevation
 995
 996        # if the camera is near the poles, then derive the azimuth from the up-vector
 997        sin_elev = np.sin(elev)
 998        if abs(sin_elev) < 0.8:
 999            azi = np.arctan2(P[1], P[0])  # azimuth from camera position
1000        else:
1001            if sin_elev < -0.8:
1002                azi = np.arctan2(upside_down_factor * up[1], upside_down_factor * up[0])
1003            else:
1004                azi = np.arctan2(-upside_down_factor * up[1], -upside_down_factor * up[0])
1005
1006        D = np.linalg.norm(P)  # distance from focal point to camera
1007
1008        # apply the change in azimuth and elevation
1009        azi_new = azi + rxf / 60
1010
1011        elev_new = elev + upside_down_factor * ryf / 60
1012
1013        # the changed elevation changes H (D stays the same)
1014        Hnew = D * np.cos(elev_new)
1015
1016        # calculate new camera position relative to focal point
1017        Pnew = np.array((Hnew * np.cos(azi_new), Hnew * np.sin(azi_new), D * np.sin(elev_new)))
1018
1019        # calculate the up-direction of the camera
1020        up_z = upside_down_factor * np.cos(elev_new)  # z follows directly from elevation
1021        up_h = upside_down_factor * np.sin(elev_new)  # horizontal component
1022        #
1023        # if upside_down:
1024        #     up_z = -up_z
1025        #     up_h = -up_h
1026
1027        up = (-up_h * np.cos(azi_new), -up_h * np.sin(azi_new), up_z)
1028
1029        new_pos = focal + Pnew
1030
1031        camera.SetViewUp(up)
1032        camera.SetPosition(new_pos)
1033
1034        camera.OrthogonalizeViewUp()
1035
1036        # Update
1037
1038        if self.GetAutoAdjustCameraClippingRange():
1039            ren.ResetCameraClippingRange()
1040
1041        if rwi.GetLightFollowCamera():
1042            ren.UpdateLightsGeometryToFollowCamera()
1043
1044        if self.callback_camera_direction_changed:
1045            self.callback_camera_direction_changed()
1046
1047        self.GetInteractor().Render()
def zoom_box(self, x1, y1, x2, y2):
1049    def zoom_box(self, x1, y1, x2, y2):
1050        """Zooms to a box"""
1051        # int width, height;
1052        #   width = abs(this->EndPosition[0] - this->StartPosition[0]);
1053        #   height = abs(this->EndPosition[1] - this->StartPosition[1]);
1054
1055        if x1 > x2:
1056            _ = x1
1057            x1 = x2
1058            x2 = _
1059        if y1 > y2:
1060            _ = y1
1061            y1 = y2
1062            y2 = _
1063
1064        width = x2 - x1
1065        height = y2 - y1
1066
1067        #   int *size = this->ren->GetSize();
1068        ren = self.GetCurrentRenderer()
1069        size = ren.GetSize()
1070        origin = ren.GetOrigin()
1071        camera = ren.GetActiveCamera()
1072
1073        # Assuming we're drawing the band on the view-plane
1074        rbcenter = (x1 + width / 2, y1 + height / 2, 0)
1075
1076        ren.SetDisplayPoint(rbcenter)
1077        ren.DisplayToView()
1078        ren.ViewToWorld()
1079
1080        worldRBCenter = ren.GetWorldPoint()
1081
1082        invw = 1.0 / worldRBCenter[3]
1083        worldRBCenter = [c * invw for c in worldRBCenter]
1084        winCenter = [origin[0] + 0.5 * size[0], origin[1] + 0.5 * size[1], 0]
1085
1086        ren.SetDisplayPoint(winCenter)
1087        ren.DisplayToView()
1088        ren.ViewToWorld()
1089
1090        worldWinCenter = ren.GetWorldPoint()
1091        invw = 1.0 / worldWinCenter[3]
1092        worldWinCenter = [c * invw for c in worldWinCenter]
1093
1094        translation = [
1095            worldRBCenter[0] - worldWinCenter[0],
1096            worldRBCenter[1] - worldWinCenter[1],
1097            worldRBCenter[2] - worldWinCenter[2],
1098        ]
1099
1100        pos = camera.GetPosition()
1101        fp = camera.GetFocalPoint()
1102        #
1103        pos = [pos[i] + translation[i] for i in range(3)]
1104        fp = [fp[i] + translation[i] for i in range(3)]
1105
1106        #
1107        camera.SetPosition(pos)
1108        camera.SetFocalPoint(fp)
1109
1110        if width > height:
1111            if width:
1112                camera.Zoom(size[0] / width)
1113        else:
1114            if height:
1115                camera.Zoom(size[1] / height)
1116
1117        self.GetInteractor().Render()

Zooms to a box

def focus_on(self, prop3D):
1119    def focus_on(self, prop3D):
1120        """Move the camera to focus on this particular prop3D"""
1121
1122        position = prop3D.GetPosition()
1123
1124        # print(f"Focus on {position}")
1125
1126        ren = self.GetCurrentRenderer()
1127        camera = ren.GetActiveCamera()
1128
1129        fp = camera.GetFocalPoint()
1130        pos = camera.GetPosition()
1131
1132        camera.SetFocalPoint(position)
1133        camera.SetPosition(
1134            position[0] - fp[0] + pos[0],
1135            position[1] - fp[1] + pos[1],
1136            position[2] - fp[2] + pos[2],
1137        )
1138
1139        if self.GetAutoAdjustCameraClippingRange():
1140            ren.ResetCameraClippingRange()
1141
1142        rwi = self.GetInteractor()
1143        if rwi.GetLightFollowCamera():
1144            ren.UpdateLightsGeometryToFollowCamera()
1145
1146        self.GetInteractor().Render()

Move the camera to focus on this particular prop3D

def dolly(self, factor):
1148    def dolly(self, factor):
1149        ren = self.GetCurrentRenderer()
1150
1151        if ren:
1152            camera = ren.GetActiveCamera()
1153
1154            if camera.GetParallelProjection():
1155                camera.SetParallelScale(camera.GetParallelScale() / factor)
1156            else:
1157                camera.Dolly(factor)
1158                if self.GetAutoAdjustCameraClippingRange():
1159                    ren.ResetCameraClippingRange()
1160
1161            # if not do_not_update:
1162            #     rwi = self.GetInteractor()
1163            #     if rwi.GetLightFollowCamera():
1164            #         ren.UpdateLightsGeometryToFollowCamera()
1165            #     # rwi.Render()
def draw_measurement(self):
1167    def draw_measurement(self):
1168        rwi = self.GetInteractor()
1169        self.end_x, self.end_y = rwi.GetEventPosition()
1170        self.draw_line(self.start_x, self.end_x, self.start_y, self.end_y)
def draw_dragged_selection(self):
1172    def draw_dragged_selection(self):
1173        rwi = self.GetInteractor()
1174        self.end_x, self.end_y = rwi.GetEventPosition()
1175        self.draw_rubber_band(self.start_x, self.end_x, self.start_y, self.end_y)
def initialize_screen_drawing(self):
1177    def initialize_screen_drawing(self):
1178        # make an image of the currently rendered image
1179
1180        rwi = self.GetInteractor()
1181        rwin = rwi.GetRenderWindow()
1182
1183        size = rwin.GetSize()
1184
1185        self._pixel_array.Initialize()
1186        self._pixel_array.SetNumberOfComponents(4)
1187        self._pixel_array.SetNumberOfTuples(size[0] * size[1])
1188
1189        front = 1  # what does this do?
1190        rwin.GetRGBACharPixelData(0, 0, size[0] - 1, size[1] - 1, front, self._pixel_array)
def draw_rubber_band(self, x1, x2, y1, y2):
1192    def draw_rubber_band(self, x1, x2, y1, y2):
1193        rwi = self.GetInteractor()
1194        rwin = rwi.GetRenderWindow()
1195
1196        size = rwin.GetSize()
1197
1198        tempPA = vtk.vtkUnsignedCharArray()
1199        tempPA.DeepCopy(self._pixel_array)
1200
1201        # check size, viewport may have been resized in the mean-time
1202        if tempPA.GetNumberOfTuples() != size[0] * size[1]:
1203            # print(
1204            #     "Starting new screen-image - viewport has resized without us knowing"
1205            # )
1206            self.initialize_screen_drawing()
1207            self.draw_rubber_band(x1, x2, y1, y2)
1208            return
1209
1210        x2 = min(x2, size[0] - 1)
1211        y2 = min(y2, size[1] - 1)
1212
1213        x2 = max(x2, 0)
1214        y2 = max(y2, 0)
1215
1216        # Modify the pixel array
1217        width = abs(x2 - x1)
1218        height = abs(y2 - y1)
1219        minx = min(x2, x1)
1220        miny = min(y2, y1)
1221
1222        # draw top and bottom
1223        for i in range(width):
1224
1225            # c = round((10*i % 254)/254) * 254  # find some alternating color
1226            c = 0
1227
1228            idx = (miny * size[0]) + minx + i
1229            tempPA.SetTuple(idx, (c, c, c, 1))
1230
1231            idx = ((miny + height) * size[0]) + minx + i
1232            tempPA.SetTuple(idx, (c, c, c, 1))
1233
1234        # draw left and right
1235        for i in range(height):
1236            # c = round((10 * i % 254) / 254) * 254  # find some alternating color
1237            c = 0
1238
1239            idx = ((miny + i) * size[0]) + minx
1240            tempPA.SetTuple(idx, (c, c, c, 1))
1241
1242            idx = idx + width
1243            tempPA.SetTuple(idx, (c, c, c, 1))
1244
1245        # and Copy back to the window
1246        rwin.SetRGBACharPixelData(0, 0, size[0] - 1, size[1] - 1, tempPA, 0)
1247        rwin.Frame()
def line2pixels(self, x1, x2, y1, y2):
1249    def line2pixels(self, x1, x2, y1, y2):
1250        """Returns the x and y values of the pixels on a line between x1,y1 and x2,y2.
1251        If start and end are identical then a single point is returned"""
1252
1253        dx = x2 - x1
1254        dy = y2 - y1
1255
1256        if dx == 0 and dy == 0:
1257            return [x1], [y1]
1258
1259        if abs(dx) > abs(dy):
1260            dhdw = dy / dx
1261            r = range(0, dx, int(dx / abs(dx)))
1262            x = [x1 + i for i in r]
1263            y = [round(y1 + dhdw * i) for i in r]
1264        else:
1265            dwdh = dx / dy
1266            r = range(0, dy, int(dy / abs(dy)))
1267            y = [y1 + i for i in r]
1268            x = [round(x1 + i * dwdh) for i in r]
1269
1270        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):
1272    def draw_line(self, x1, x2, y1, y2):
1273        rwi = self.GetInteractor()
1274        rwin = rwi.GetRenderWindow()
1275
1276        size = rwin.GetSize()
1277
1278        x1 = min(max(x1, 0), size[0])
1279        x2 = min(max(x2, 0), size[0])
1280        y1 = min(max(y1, 0), size[1])
1281        y2 = min(max(y2, 0), size[1])
1282
1283        tempPA = vtk.vtkUnsignedCharArray()
1284        tempPA.DeepCopy(self._pixel_array)
1285
1286        xs, ys = self.line2pixels(x1, x2, y1, y2)
1287        for x, y in zip(xs, ys):
1288            idx = (y * size[0]) + x
1289            tempPA.SetTuple(idx, (0, 0, 0, 1))
1290
1291        # and Copy back to the window
1292        rwin.SetRGBACharPixelData(0, 0, size[0] - 1, size[1] - 1, tempPA, 0)
1293
1294        camera = self.GetCurrentRenderer().GetActiveCamera()
1295        scale = camera.GetParallelScale()
1296
1297        # Set/Get the scaling used for a parallel projection, i.e.
1298        #
1299        # the half of the height of the viewport in world-coordinate distances.
1300        # The default is 1. Note that the "scale" parameter works as an "inverse scale"
1301        #  larger numbers produce smaller images.
1302        # This method has no effect in perspective projection mode
1303
1304        half_height = size[1] / 2
1305        # half_height [px] = scale [world-coordinates]
1306
1307        length = ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5
1308        meters_per_pixel = scale / half_height
1309        meters = length * meters_per_pixel
1310
1311        if camera.GetParallelProjection():
1312            print(f"Line length = {length} px = {meters} m")
1313        else:
1314            print("Need to be in non-perspective mode to measure. Press 2 or 3 to get there")
1315
1316        if self.callback_measure:
1317            self.callback_measure(meters)
1318
1319        # # can we add something to the window here?
1320        # freeType = vtk.FreeTypeTools.GetInstance()
1321        # textProperty = vtk.vtkTextProperty()
1322        # textProperty.SetJustificationToLeft()
1323        # textProperty.SetFontSize(24)
1324        # textProperty.SetOrientation(25)
1325        #
1326        # textImage = vtk.vtkImageData()
1327        # freeType.RenderString(textProperty, "a somewhat longer text", 72, textImage)
1328        # # this does not give an error, assume it works
1329        # #
1330        # textImage.GetDimensions()
1331        # textImage.GetExtent()
1332        #
1333        # # # Now put the textImage in the RenderWindow
1334        # rwin.SetRGBACharPixelData(0, 0, size[0] - 1, size[1] - 1, textImage, 0)
1335
1336        rwin.Frame()