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

callbackStartDrag is called when initializing the drag. This is when to assign actors and other data to draginfo.

callbackEndDrag is called when the drag is accepted.

Responding to other events

callbackCameraDirectionChanged : 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:
from vedo import *
settings.enable_default_keyboard_callbacks = False
settings.enable_default_mouse_callbacks = False
mesh = Mesh(dataurl+"cow.vtk")
mode = interactor_modes.BlenderStyle()
plt = Plotter().user_mode(mode)
plt.show(mesh, axes=1)
BlenderStyle()
278    def __init__(self):
279
280        super().__init__()
281
282        self.interactor = None
283        self.renderer = None
284
285        # callbackSelect is called whenever one or mode props are selected.
286        # callback will be called with a list of props of which the first entry
287        # is prop closest to the camera.
288        self.callbackSelect = None
289        self.callbackStartDrag = None
290        self.callbackEndDrag = None
291        self.callbackEscapeKey = None
292        self.callbackDeleteKey = None
293        self.callbackFocusKey = None
294        self.callbackAnyKey = None
295        self.callbackMeasure = None  # callback with argument float (meters)
296        self.callbackCameraDirectionChanged = None
297
298        # active drag
299        # assigned to a _BlenderStyleDragInfo object when dragging is active
300        self.draginfo: _BlenderStyleDragInfo or None = None
301
302        # picking
303        self.picked_props = []  # will be filled by latest pick
304
305        # settings
306        self.mouse_motion_factor = 20
307        self.mouse_wheel_motion_factor = 0.1
308        self.zoom_motion_factor = 0.25
309
310        # internals
311        self.start_x = 0  # start of a drag
312        self.start_y = 0
313        self.end_x = 0
314        self.end_y = 0
315
316        self.middle_mouse_lock = False
317        self.middle_mouse_lock_actor = None  # will be created when required
318
319        # Special Modes
320        self._is_box_zooming = False
321
322        # holds an image of the renderer output at the start of a drawing event
323        self._pixel_array = vtk.vtkUnsignedCharArray()
324
325        self._upside_down = False
326
327        self._left_button_down = False
328        self._middle_button_down = False
329
330        self.AddObserver("RightButtonPressEvent", self.RightButtonPress)
331        self.AddObserver("RightButtonReleaseEvent", self.RightButtonRelease)
332        self.AddObserver("MiddleButtonPressEvent", self.MiddleButtonPress)
333        self.AddObserver("MiddleButtonReleaseEvent", self.MiddleButtonRelease)
334        self.AddObserver("MouseWheelForwardEvent", self.MouseWheelForward)
335        self.AddObserver("MouseWheelBackwardEvent", self.MouseWheelBackward)
336        self.AddObserver("LeftButtonPressEvent", self.LeftButtonPress)
337        self.AddObserver("LeftButtonReleaseEvent", self.LeftButtonRelease)
338        self.AddObserver("MouseMoveEvent", self.MouseMove)
339        self.AddObserver("WindowResizeEvent", self.WindowResized)
340        # ^does not seem to fire!
341        self.AddObserver("KeyPressEvent", self.KeyPress)
342        self.AddObserver("KeyReleaseEvent", self.KeyRelease)
def RightButtonPress(self, obj, event):
344    def RightButtonPress(self, obj, event):
345        pass
def RightButtonRelease(self, obj, event):
347    def RightButtonRelease(self, obj, event):
348        pass
def MiddleButtonPress(self, obj, event):
350    def MiddleButtonPress(self, obj, event):
351        self._middle_button_down = True
def MiddleButtonRelease(self, obj, event):
353    def MiddleButtonRelease(self, obj, event):
354        self._middle_button_down = False
355
356        # perform middle button focus event if ALT is down
357        if self.GetInteractor().GetAltKey():
358            # print("Middle button released while ALT is down")
359
360            # try to pick an object at the current mouse position
361            rwi = self.GetInteractor()
362            self.start_x, self.start_y = rwi.GetEventPosition()
363            props = self.PerformPickingOnSelection()
364
365            if props:
366                self.FocusOn(props[0])
def MouseWheelBackward(self, obj, event):
368    def MouseWheelBackward(self, obj, event):
369        self.MoveMouseWheel(-1)
def MouseWheelForward(self, obj, event):
371    def MouseWheelForward(self, obj, event):
372        self.MoveMouseWheel(1)
def MouseMove(self, obj, event):
374    def MouseMove(self, obj, event):
375
376        interactor = self.GetInteractor()
377
378        # Find the renderer that is active below the current mouse position
379        x, y = interactor.GetEventPosition()
380        self.FindPokedRenderer(x, y)
381        # sets the current renderer
382        # [this->SetCurrentRenderer(this->Interactor->FindPokedRenderer(x, y));]
383
384        Shift = interactor.GetShiftKey()
385        Ctrl = interactor.GetControlKey()
386        Alt = interactor.GetAltKey()
387
388        MiddleButton = self._middle_button_down or self.middle_mouse_lock
389
390        # start with the special modes
391        if self._is_box_zooming:
392            self.DrawDraggedSelection()
393        elif MiddleButton and not Shift and not Ctrl and not Alt:
394            self.Rotate()
395        elif MiddleButton and Shift and not Ctrl and not Alt:
396            self.Pan()
397        elif MiddleButton and Ctrl and not Shift and not Alt:
398            self.Zoom()  # Dolly
399        elif self.draginfo is not None:
400            self.ExecuteDrag()
401        elif self._left_button_down and Ctrl and Shift:
402            self.DrawMeasurement()
403        elif self._left_button_down:
404            self.DrawDraggedSelection()
405
406        self.InvokeEvent("InteractionEvent", None)
def MoveMouseWheel(self, direction):
408    def MoveMouseWheel(self, direction):
409        rwi = self.GetInteractor()
410
411        # Find the renderer that is active below the current mouse position
412        x, y = rwi.GetEventPosition()
413        self.FindPokedRenderer(x, y)
414        # sets the current renderer
415        # [this->SetCurrentRenderer(this->Interactor->FindPokedRenderer(x, y));]
416
417        # The movement
418
419        CurrentRenderer = self.GetCurrentRenderer()
420
421        #   // Calculate the focal depth since we'll be using it a lot
422        camera = CurrentRenderer.GetActiveCamera()
423        viewFocus = camera.GetFocalPoint()
424
425        temp_out = [0, 0, 0]
426        self.ComputeWorldToDisplay(
427            CurrentRenderer, viewFocus[0], viewFocus[1], viewFocus[2], temp_out
428        )
429        focalDepth = temp_out[2]
430
431        newPickPoint = [0, 0, 0, 0]
432        x, y = rwi.GetEventPosition()
433        self.ComputeDisplayToWorld(CurrentRenderer, x, y, focalDepth, newPickPoint)
434
435        #   // Has to recalc old mouse point since the viewport has moved,
436        #   // so can't move it outside the loop
437
438        oldPickPoint = [0, 0, 0, 0]
439        # xp, yp = rwi.GetLastEventPosition()
440
441        # find the center of the window
442        size = rwi.GetRenderWindow().GetSize()
443        xp = size[0] / 2
444        yp = size[1] / 2
445
446        self.ComputeDisplayToWorld(CurrentRenderer, xp, yp, focalDepth, oldPickPoint)
447        #
448        #   // Camera motion is reversed
449        #
450        move_factor = -1 * self.zoom_motion_factor * direction
451
452        motionVector = (
453            move_factor * (oldPickPoint[0] - newPickPoint[0]),
454            move_factor * (oldPickPoint[1] - newPickPoint[1]),
455            move_factor * (oldPickPoint[2] - newPickPoint[2]),
456        )
457
458        viewFocus = camera.GetFocalPoint()  # do we need to do this again? Already did this
459        viewPoint = camera.GetPosition()
460
461        camera.SetFocalPoint(
462            motionVector[0] + viewFocus[0],
463            motionVector[1] + viewFocus[1],
464            motionVector[2] + viewFocus[2],
465        )
466        camera.SetPosition(
467            motionVector[0] + viewPoint[0],
468            motionVector[1] + viewPoint[1],
469            motionVector[2] + viewPoint[2],
470        )
471
472        # the Zooming
473        factor = self.mouse_motion_factor * self.mouse_wheel_motion_factor
474        self.ZoomByStep(direction * factor)
def ZoomByStep(self, step):
476    def ZoomByStep(self, step):
477        CurrentRenderer = self.GetCurrentRenderer()
478
479        if CurrentRenderer:
480            self.StartDolly()
481            self.Dolly(pow(1.1, step))
482            self.EndDolly()
def LeftButtonPress(self, obj, event):
484    def LeftButtonPress(self, obj, event):
485
486        if self._is_box_zooming:
487            return
488        if self.draginfo:
489            return
490
491        self._left_button_down = True
492
493        interactor = self.GetInteractor()
494        Shift = interactor.GetShiftKey()
495        Ctrl = interactor.GetControlKey()
496
497        if Shift and Ctrl:
498            if not self.GetCurrentRenderer().GetActiveCamera().GetParallelProjection():
499                self.ToggleParallelProjection()
500
501        rwi = self.GetInteractor()
502        self.start_x, self.start_y = rwi.GetEventPosition()
503        self.end_x = self.start_x
504        self.end_y = self.start_y
505
506        self.InitializeScreenDrawing()
def LeftButtonRelease(self, obj, event):
508    def LeftButtonRelease(self, obj, event):
509
510        if self._is_box_zooming:
511            self._is_box_zooming = False
512            self.ZoomBox(self.start_x, self.start_y, self.end_x, self.end_y)
513            return
514
515        if self.draginfo:
516            self.FinishDrag()
517            return
518
519        self._left_button_down = False
520
521        interactor = self.GetInteractor()
522
523        Shift = interactor.GetShiftKey()
524        Ctrl = interactor.GetControlKey()
525        Alt = interactor.GetAltKey()
526
527        if Ctrl and Shift:
528            pass  # we were drawing the measurement
529
530        else:
531            if self.callbackSelect:
532                props = self.PerformPickingOnSelection()
533
534                if props:  # only call back if anything was selected
535                    self.picked_props = tuple(props)
536                    self.callbackSelect(props)
537
538        # remove the selection rubber band / line
539        self.DoRender()
def KeyPress(self, obj, event):
541    def KeyPress(self, obj, event):
542
543        key = obj.GetKeySym()
544        KEY = key.upper()
545
546        # logging.info(f"Key Press: {key}")
547        if self.callbackAnyKey:
548            if self.callbackAnyKey(key):
549                return
550
551        if KEY == "M":
552            self.middle_mouse_lock = not self.middle_mouse_lock
553            self.UpdateMiddleMouseButtonLockActor()
554        elif KEY == "G":
555            if self.draginfo is not None:
556                self.FinishDrag()
557            else:
558                if self.callbackStartDrag:
559                    self.callbackStartDrag()
560                else:
561                    self.StartDrag()
562                    # internally calls end-drag if drag is already active
563        elif KEY == "ESCAPE":
564            if self.callbackEscapeKey:
565                self.callbackEscapeKey()
566            if self.draginfo is not None:
567                self.CancelDrag()
568        elif KEY == "DELETE":
569            if self.callbackDeleteKey:
570                self.callbackDeleteKey()
571        elif KEY == "RETURN":
572            if self.draginfo:
573                self.FinishDrag()
574        elif KEY == "SPACE":
575            self.middle_mouse_lock = True
576            # self.UpdateMiddleMouseButtonLockActor()
577            # self.GrabFocus("MouseMoveEvent", self)
578            # # TODO: grab and release focus; possible from python?
579        elif KEY == "B":
580            self._is_box_zooming = True
581            rwi = self.GetInteractor()
582            self.start_x, self.start_y = rwi.GetEventPosition()
583            self.end_x = self.start_x
584            self.end_y = self.start_y
585            self.InitializeScreenDrawing()
586        elif KEY in ('2', '3'):
587            self.ToggleParallelProjection()
588
589        elif KEY == "A":
590            self.ZoomFit()
591        elif KEY == "X":
592            self.SetViewX()
593        elif KEY == "Y":
594            self.SetViewY()
595        elif KEY == "Z":
596            self.SetViewZ()
597        elif KEY == "LEFT":
598            self.RotateDiscreteStep(1)
599        elif KEY == "RIGHT":
600            self.RotateDiscreteStep(-1)
601        elif KEY == "UP":
602            self.RotateTurtableBy(0, 10)
603        elif KEY == "DOWN":
604            self.RotateTurtableBy(0, -10)
605        elif KEY == "PLUS":
606            self.ZoomByStep(2)
607        elif KEY == "MINUS":
608            self.ZoomByStep(-2)
609        elif KEY == "F":
610            if self.callbackFocusKey:
611                self.callbackFocusKey()
612
613        self.InvokeEvent("InteractionEvent", None)
def KeyRelease(self, obj, event):
615    def KeyRelease(self, obj, event):
616
617        key = obj.GetKeySym()
618        KEY = key.upper()
619
620        # print(f"Key release: {key}")
621
622        if KEY == "SPACE":
623            if self.middle_mouse_lock:
624                self.middle_mouse_lock = False
625                self.UpdateMiddleMouseButtonLockActor()
def WindowResized(self):
627    def WindowResized(self):
628        # print("window resized")
629        self.InitializeScreenDrawing()
def RotateDiscreteStep(self, movement_direction, step=22.5):
631    def RotateDiscreteStep(self, movement_direction, step=22.5):
632        """Rotates CW or CCW to the nearest 45 deg angle
633        - includes some fuzzyness to determine about which axis"""
634
635        CurrentRenderer = self.GetCurrentRenderer()
636        camera = CurrentRenderer.GetActiveCamera()
637
638        step = np.deg2rad(step)
639
640        direction = -np.array(camera.GetViewPlaneNormal())  # current camera direction
641
642        if abs(direction[2]) < 0.7:
643            # horizontal view, rotate camera position about Z-axis
644            angle = np.arctan2(direction[1], direction[0])
645
646            # find the nearest angle that is an integer number of steps
647            if movement_direction > 0:
648                angle = step * np.floor((angle + 0.1 * step) / step) + step
649            else:
650                angle = -step * np.floor(-(angle - 0.1 * step) / step) - step
651
652            dist = np.linalg.norm(direction[:2])
653
654            direction[0] = np.cos(angle) * dist
655            direction[1] = np.sin(angle) * dist
656
657            self.SetCameraDirection(direction)
658
659        else:  # Top or bottom like view - rotate camera "up" direction
660
661            up = np.array(camera.GetViewUp())
662
663            angle = np.arctan2(up[1], up[0])
664
665            # find the nearest angle that is an integer number of steps
666            if movement_direction > 0:
667                angle = step * np.floor((angle + 0.1 * step) / step) + step
668            else:
669                angle = -step * np.floor(-(angle - 0.1 * step) / step) - step
670
671            dist = np.linalg.norm(up[:2])
672
673            up[0] = np.cos(angle) * dist
674            up[1] = np.sin(angle) * dist
675
676            camera.SetViewUp(up)
677            camera.OrthogonalizeViewUp()
678
679            self.DoRender()

Rotates CW or CCW to the nearest 45 deg angle

  • includes some fuzzyness to determine about which axis
def ToggleParallelProjection(self):
681    def ToggleParallelProjection(self):
682        renderer = self.GetCurrentRenderer()
683        camera = renderer.GetActiveCamera()
684        camera.SetParallelProjection(not bool(camera.GetParallelProjection()))
685        self.DoRender()
def SetViewX(self):
687    def SetViewX(self):
688        self.SetCameraPlaneDirection((1, 0, 0))
def SetViewY(self):
690    def SetViewY(self):
691        self.SetCameraPlaneDirection((0, 1, 0))
def SetViewZ(self):
693    def SetViewZ(self):
694        self.SetCameraPlaneDirection((0, 0, 1))
def ZoomFit(self):
696    def ZoomFit(self):
697        self.GetCurrentRenderer().ResetCamera()
698        self.DoRender()
def SetCameraPlaneDirection(self, direction):
700    def SetCameraPlaneDirection(self, direction):
701        """Sets the camera to display a plane of which direction is the normal
702        - includes logic to reverse the direction if benificial"""
703
704        CurrentRenderer = self.GetCurrentRenderer()
705        camera = CurrentRenderer.GetActiveCamera()
706
707        direction = np.array(direction)
708
709        normal = camera.GetViewPlaneNormal()
710        # can not set the normal, need to change the position to do that
711
712        current_alignment = np.dot(normal, -direction)
713        # print(f"Current alignment = {current_alignment}")
714
715        if abs(current_alignment) > 0.9999:
716            # print("toggling")
717            direction = -np.array(normal)
718        elif current_alignment > 0:  # find the nearest plane
719            # print("reversing to find nearest")
720            direction = -direction
721
722        self.SetCameraDirection(-direction)

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

  • includes logic to reverse the direction if benificial
def SetCameraDirection(self, direction):
724    def SetCameraDirection(self, direction):
725        """Sets the camera to this direction, sets view up if horizontal enough"""
726        direction = np.array(direction)
727
728        CurrentRenderer = self.GetCurrentRenderer()
729        camera = CurrentRenderer.GetActiveCamera()
730        rwi = self.GetInteractor()
731
732        pos = np.array(camera.GetPosition())
733        focal = np.array(camera.GetFocalPoint())
734        dist = np.linalg.norm(pos - focal)
735
736        pos = focal - dist * direction
737        camera.SetPosition(pos)
738
739        if abs(direction[2]) < 0.9:
740            camera.SetViewUp(0, 0, 1)
741        elif direction[2] > 0.9:
742            camera.SetViewUp(0, -1, 0)
743        else:
744            camera.SetViewUp(0, 1, 0)
745
746        camera.OrthogonalizeViewUp()
747
748        if self.GetAutoAdjustCameraClippingRange():
749            CurrentRenderer.ResetCameraClippingRange()
750
751        if rwi.GetLightFollowCamera():
752            CurrentRenderer.UpdateLightsGeometryToFollowCamera()
753
754        if self.callbackCameraDirectionChanged:
755            self.callbackCameraDirectionChanged()
756
757        self.DoRender()

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

def PerformPickingOnSelection(self):
759    def PerformPickingOnSelection(self):
760        """Preforms prop3d picking on the current dragged selection
761
762        If the distance between the start and endpoints is less than the threshold
763        then a SINGLE prop3d is picked along the line
764
765        the selection area is drawn by the rubber band and is defined by
766        self.start_x, self.start_y, self.end_x, self.end_y
767        """
768        renderer = self.GetCurrentRenderer()
769
770        assemblyPath = renderer.PickProp(self.start_x, self.start_y, self.end_x, self.end_y)
771
772        # re-pick in larger area if nothing is returned
773        if not assemblyPath:
774            self.start_x -= 2
775            self.end_x += 2
776            self.start_y -= 2
777            self.end_y += 2
778            assemblyPath = renderer.PickProp(self.start_x, self.start_y, self.end_x, self.end_y)
779
780        # The nearest prop (by Z-value)
781        if assemblyPath:
782            assert (
783                assemblyPath.GetNumberOfItems() == 1
784            ), "Wrong assumption on number of returned nodes when picking"
785            nearest_prop = assemblyPath.GetItemAsObject(0).GetViewProp()
786
787            # all props
788            collection = renderer.GetPickResultProps()
789            props = [collection.GetItemAsObject(i) for i in range(collection.GetNumberOfItems())]
790
791            props.remove(nearest_prop)
792            props.insert(0, nearest_prop)
793
794            return props
795
796        else:
797            return []

Preforms prop3d picking on the current dragged selection

If the distance between the start and endpoints is less than the threshold then a SINGLE prop3d 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 StartDrag(self):
801    def StartDrag(self):
802        if self.callbackStartDrag:
803            # print("Calling callbackStartDrag")
804            self.callbackStartDrag()
805            return
806        else:  # grab the current selection
807            if self.picked_props:
808                self.StartDragOnProps(self.picked_props)
809            else:
810                pass
811                # print('Can not start drag, nothing selected and callbackStartDrag not assigned')
def FinishDrag(self):
813    def FinishDrag(self):
814        # print('Finished drag')
815        if self.callbackEndDrag:
816            # reset actor positions as actors positions will be controlled by called functions
817            for pos0, actor in zip(
818                self.draginfo.dragged_actors_original_positions, self.draginfo.actors_dragging
819            ):
820                actor.SetPosition(pos0)
821            self.callbackEndDrag(self.draginfo)
822
823        self.draginfo = None
def StartDragOnProps(self, props):
825    def StartDragOnProps(self, props):
826        """Starts drag on the provided props (actors) by filling self.draginfo"""
827        if self.draginfo is not None:
828            self.FinishDrag()
829            return
830
831        # print('Starting drag')
832
833        # create and fill drag-info
834        draginfo = _BlenderStyleDragInfo()
835
836        #
837        # draginfo.dragged_node = node
838        #
839        # # find all actors and outlines corresponding to this node
840        # actors = [*self.actor_from_node(node).actors.values()]
841        # outlines = [ol.outline_actor for ol in self.node_outlines if ol.parent_vp_actor in actors]
842
843        draginfo.actors_dragging = props  # [*actors, *outlines]
844
845        for a in draginfo.actors_dragging:
846            draginfo.dragged_actors_original_positions.append(a.GetPosition())  # numpy ndarray
847
848        # Get the start position of the drag in 3d
849
850        rwi = self.GetInteractor()
851        CurrentRenderer = self.GetCurrentRenderer()
852        camera = CurrentRenderer.GetActiveCamera()
853        viewFocus = camera.GetFocalPoint()
854
855        temp_out = [0, 0, 0]
856        self.ComputeWorldToDisplay(
857            CurrentRenderer, viewFocus[0], viewFocus[1], viewFocus[2], temp_out
858        )
859        focalDepth = temp_out[2]
860
861        newPickPoint = [0, 0, 0, 0]
862        x, y = rwi.GetEventPosition()
863        self.ComputeDisplayToWorld(CurrentRenderer, x, y, focalDepth, newPickPoint)
864
865        mouse_pos_3d = np.array(newPickPoint[:3])
866
867        draginfo.start_position_3d = mouse_pos_3d
868
869        self.draginfo = draginfo

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

def ExecuteDrag(self):
871    def ExecuteDrag(self):
872
873        rwi = self.GetInteractor()
874        CurrentRenderer = self.GetCurrentRenderer()
875
876        camera = CurrentRenderer.GetActiveCamera()
877        viewFocus = camera.GetFocalPoint()
878
879        # Get the picked point in 3d
880
881        temp_out = [0, 0, 0]
882        self.ComputeWorldToDisplay(
883            CurrentRenderer, viewFocus[0], viewFocus[1], viewFocus[2], temp_out
884        )
885        focalDepth = temp_out[2]
886
887        newPickPoint = [0, 0, 0, 0]
888        x, y = rwi.GetEventPosition()
889        self.ComputeDisplayToWorld(CurrentRenderer, x, y, focalDepth, newPickPoint)
890
891        mouse_pos_3d = np.array(newPickPoint[:3])
892
893        # compute the delta and execute
894
895        delta = np.array(mouse_pos_3d) - self.draginfo.start_position_3d
896        # print(f'Delta = {delta}')
897        view_normal = np.array(self.GetCurrentRenderer().GetActiveCamera().GetViewPlaneNormal())
898
899        delta_inplane = delta - view_normal * np.dot(delta, view_normal)
900        # print(f'delta_inplane = {delta_inplane}')
901
902        for pos0, actor in zip(
903            self.draginfo.dragged_actors_original_positions, self.draginfo.actors_dragging
904        ):
905            m = actor.GetUserMatrix()
906            if m:
907                print("UserMatrices/transforms not supported")
908                # m.Invert() #inplace
909                # rotated = m.MultiplyFloatPoint([*delta_inplane, 1])
910                # actor.SetPosition(pos0 + np.array(rotated[:3]))
911            actor.SetPosition(pos0 + delta_inplane)
912
913        # print(f'Set position to {pos0 + delta_inplane}')
914
915        self.draginfo.delta = delta_inplane  # store the current delta
916
917        # self.GetInteractor().Render()
918        self.DoRender()
def CancelDrag(self):
920    def CancelDrag(self):
921        """Cancels the drag and restored the original positions of all dragged actors"""
922        for pos0, actor in zip(
923            self.draginfo.dragged_actors_original_positions, self.draginfo.actors_dragging
924        ):
925            actor.SetPosition(pos0)
926        self.draginfo = None
927        self.DoRender()

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

def Zoom(self):
931    def Zoom(self):
932        rwi = self.GetInteractor()
933        x, y = rwi.GetEventPosition()
934        xp, yp = rwi.GetLastEventPosition()
935
936        direction = y - yp
937        self.MoveMouseWheel(direction / 10)

Zoom(self) -> None C++: virtual void Zoom()

def Pan(self):
939    def Pan(self):
940
941        CurrentRenderer = self.GetCurrentRenderer()
942
943        if CurrentRenderer:
944
945            rwi = self.GetInteractor()
946
947            #   // Calculate the focal depth since we'll be using it a lot
948            camera = CurrentRenderer.GetActiveCamera()
949            viewFocus = camera.GetFocalPoint()
950
951            temp_out = [0, 0, 0]
952            self.ComputeWorldToDisplay(
953                CurrentRenderer, viewFocus[0], viewFocus[1], viewFocus[2], temp_out
954            )
955            focalDepth = temp_out[2]
956
957            newPickPoint = [0, 0, 0, 0]
958            x, y = rwi.GetEventPosition()
959            self.ComputeDisplayToWorld(CurrentRenderer, x, y, focalDepth, newPickPoint)
960
961            #   // Has to recalc old mouse point since the viewport has moved,
962            #   // so can't move it outside the loop
963
964            oldPickPoint = [0, 0, 0, 0]
965            xp, yp = rwi.GetLastEventPosition()
966            self.ComputeDisplayToWorld(CurrentRenderer, xp, yp, focalDepth, oldPickPoint)
967            #
968            #   // Camera motion is reversed
969            #
970            motionVector = (
971                oldPickPoint[0] - newPickPoint[0],
972                oldPickPoint[1] - newPickPoint[1],
973                oldPickPoint[2] - newPickPoint[2],
974            )
975
976            viewFocus = camera.GetFocalPoint()  # do we need to do this again? Already did this
977            viewPoint = camera.GetPosition()
978
979            camera.SetFocalPoint(
980                motionVector[0] + viewFocus[0],
981                motionVector[1] + viewFocus[1],
982                motionVector[2] + viewFocus[2],
983            )
984            camera.SetPosition(
985                motionVector[0] + viewPoint[0],
986                motionVector[1] + viewPoint[1],
987                motionVector[2] + viewPoint[2],
988            )
989
990            if rwi.GetLightFollowCamera():
991                CurrentRenderer.UpdateLightsGeometryToFollowCamera()
992
993            self.DoRender()

Pan(self) -> None C++: virtual void Pan()

def Rotate(self):
 995    def Rotate(self):
 996
 997        CurrentRenderer = self.GetCurrentRenderer()
 998
 999        if CurrentRenderer:
1000
1001            rwi = self.GetInteractor()
1002            dx = rwi.GetEventPosition()[0] - rwi.GetLastEventPosition()[0]
1003            dy = rwi.GetEventPosition()[1] - rwi.GetLastEventPosition()[1]
1004
1005            size = CurrentRenderer.GetRenderWindow().GetSize()
1006            delta_elevation = -20.0 / size[1]
1007            delta_azimuth = -20.0 / size[0]
1008
1009            rxf = dx * delta_azimuth * self.mouse_motion_factor
1010            ryf = dy * delta_elevation * self.mouse_motion_factor
1011
1012            self.RotateTurtableBy(rxf, ryf)

Rotate(self) -> None C++: virtual void Rotate()

These methods for the different interactions in different modes are overridden in subclasses to perform the correct motion. Since they might be called from OnTimer, they do not have mouse coord parameters (use interactor's GetEventPosition and GetLastEventPosition)

def RotateTurtableBy(self, rxf, ryf):
1014    def RotateTurtableBy(self, rxf, ryf):
1015
1016        CurrentRenderer = self.GetCurrentRenderer()
1017        rwi = self.GetInteractor()
1018
1019        # rfx is rotation about the global Z vector (turn-table mode)
1020        # rfy is rotation about the side vector
1021
1022        camera = CurrentRenderer.GetActiveCamera()
1023        campos = np.array(camera.GetPosition())
1024        focal = np.array(camera.GetFocalPoint())
1025        up = camera.GetViewUp()
1026        upside_down_factor = -1 if up[2] < 0 else 1
1027
1028        # rotate about focal point
1029
1030        P = campos - focal  # camera position
1031
1032        # Rotate left/right about the global Z axis
1033        H = np.linalg.norm(P[:2])  # horizontal distance of camera to focal point
1034        elev = np.arctan2(P[2], H)  # elevation
1035
1036        # if the camera is near the poles, then derive the azimuth from the up-vector
1037        sin_elev = np.sin(elev)
1038        if abs(sin_elev) < 0.8:
1039            azi = np.arctan2(P[1], P[0])  # azimuth from camera position
1040        else:
1041            if sin_elev < -0.8:
1042                azi = np.arctan2(upside_down_factor * up[1], upside_down_factor * up[0])
1043            else:
1044                azi = np.arctan2(-upside_down_factor * up[1], -upside_down_factor * up[0])
1045
1046        D = np.linalg.norm(P)  # distance from focal point to camera
1047
1048        # apply the change in azimuth and elevation
1049        azi_new = azi + rxf / 60
1050
1051        elev_new = elev + upside_down_factor * ryf / 60
1052
1053        # the changed elevation changes H (D stays the same)
1054        Hnew = D * np.cos(elev_new)
1055
1056        # calculate new camera position relative to focal point
1057        Pnew = np.array((Hnew * np.cos(azi_new), Hnew * np.sin(azi_new), D * np.sin(elev_new)))
1058
1059        # calculate the up-direction of the camera
1060        up_z = upside_down_factor * np.cos(elev_new)  # z follows directly from elevation
1061        up_h = upside_down_factor * np.sin(elev_new)  # horizontal component
1062        #
1063        # if upside_down:
1064        #     up_z = -up_z
1065        #     up_h = -up_h
1066
1067        up = (-up_h * np.cos(azi_new), -up_h * np.sin(azi_new), up_z)
1068
1069        new_pos = focal + Pnew
1070
1071        camera.SetViewUp(up)
1072        camera.SetPosition(new_pos)
1073
1074        camera.OrthogonalizeViewUp()
1075
1076        # Update
1077
1078        if self.GetAutoAdjustCameraClippingRange():
1079            CurrentRenderer.ResetCameraClippingRange()
1080
1081        if rwi.GetLightFollowCamera():
1082            CurrentRenderer.UpdateLightsGeometryToFollowCamera()
1083
1084        if self.callbackCameraDirectionChanged:
1085            self.callbackCameraDirectionChanged()
1086
1087        self.DoRender()
def ZoomBox(self, x1, y1, x2, y2):
1089    def ZoomBox(self, x1, y1, x2, y2):
1090        """Zooms to a box"""
1091        # int width, height;
1092        #   width = abs(this->EndPosition[0] - this->StartPosition[0]);
1093        #   height = abs(this->EndPosition[1] - this->StartPosition[1]);
1094
1095        if x1 > x2:
1096            _ = x1
1097            x1 = x2
1098            x2 = _
1099        if y1 > y2:
1100            _ = y1
1101            y1 = y2
1102            y2 = _
1103
1104        width = x2 - x1
1105        height = y2 - y1
1106
1107        #   int *size = this->CurrentRenderer->GetSize();
1108        CurrentRenderer = self.GetCurrentRenderer()
1109        size = CurrentRenderer.GetSize()
1110        origin = CurrentRenderer.GetOrigin()
1111        camera = CurrentRenderer.GetActiveCamera()
1112
1113        # Assuming we're drawing the band on the view-plane
1114        rbcenter = (x1 + width / 2, y1 + height / 2, 0)
1115
1116        CurrentRenderer.SetDisplayPoint(rbcenter)
1117        CurrentRenderer.DisplayToView()
1118        CurrentRenderer.ViewToWorld()
1119
1120        worldRBCenter = CurrentRenderer.GetWorldPoint()
1121
1122        invw = 1.0 / worldRBCenter[3]
1123        worldRBCenter = [c * invw for c in worldRBCenter]
1124        winCenter = [origin[0] + 0.5 * size[0], origin[1] + 0.5 * size[1], 0]
1125
1126        CurrentRenderer.SetDisplayPoint(winCenter)
1127        CurrentRenderer.DisplayToView()
1128        CurrentRenderer.ViewToWorld()
1129
1130        worldWinCenter = CurrentRenderer.GetWorldPoint()
1131        invw = 1.0 / worldWinCenter[3]
1132        worldWinCenter = [c * invw for c in worldWinCenter]
1133
1134        translation = [
1135            worldRBCenter[0] - worldWinCenter[0],
1136            worldRBCenter[1] - worldWinCenter[1],
1137            worldRBCenter[2] - worldWinCenter[2],
1138        ]
1139
1140        pos = camera.GetPosition()
1141        fp = camera.GetFocalPoint()
1142        #
1143        pos = [pos[i] + translation[i] for i in range(3)]
1144        fp = [fp[i] + translation[i] for i in range(3)]
1145
1146        #
1147        camera.SetPosition(pos)
1148        camera.SetFocalPoint(fp)
1149
1150        if width > height:
1151            if width:
1152                camera.Zoom(size[0] / width)
1153        else:
1154            if height:
1155                camera.Zoom(size[1] / height)
1156
1157        self.DoRender()

Zooms to a box

def FocusOn(self, prop3D):
1159    def FocusOn(self, prop3D):
1160        """Move the camera to focus on this particular prop3D"""
1161
1162        position = prop3D.GetPosition()
1163
1164        # print(f"Focus on {position}")
1165
1166        CurrentRenderer = self.GetCurrentRenderer()
1167        camera = CurrentRenderer.GetActiveCamera()
1168
1169        fp = camera.GetFocalPoint()
1170        pos = camera.GetPosition()
1171
1172        camera.SetFocalPoint(position)
1173        camera.SetPosition(
1174            position[0] - fp[0] + pos[0],
1175            position[1] - fp[1] + pos[1],
1176            position[2] - fp[2] + pos[2],
1177        )
1178
1179        if self.GetAutoAdjustCameraClippingRange():
1180            CurrentRenderer.ResetCameraClippingRange()
1181
1182        rwi = self.GetInteractor()
1183        if rwi.GetLightFollowCamera():
1184            CurrentRenderer.UpdateLightsGeometryToFollowCamera()
1185
1186        self.DoRender()

Move the camera to focus on this particular prop3D

def Dolly(self, factor):
1188    def Dolly(self, factor):
1189        CurrentRenderer = self.GetCurrentRenderer()
1190
1191        if CurrentRenderer:
1192            camera = CurrentRenderer.GetActiveCamera()
1193
1194            if camera.GetParallelProjection():
1195                camera.SetParallelScale(camera.GetParallelScale() / factor)
1196            else:
1197                camera.Dolly(factor)
1198                if self.GetAutoAdjustCameraClippingRange():
1199                    CurrentRenderer.ResetCameraClippingRange()
1200
1201            # if not do_not_update:
1202            #     rwi = self.GetInteractor()
1203            #     if rwi.GetLightFollowCamera():
1204            #         CurrentRenderer.UpdateLightsGeometryToFollowCamera()
1205            #     # rwi.Render()
1206            #     self.DoRender()

Dolly(self) -> None C++: virtual void Dolly()

def DrawMeasurement(self):
1208    def DrawMeasurement(self):
1209        rwi = self.GetInteractor()
1210        self.end_x, self.end_y = rwi.GetEventPosition()
1211        self.DrawLine(self.start_x, self.end_x, self.start_y, self.end_y)
def DrawDraggedSelection(self):
1213    def DrawDraggedSelection(self):
1214        rwi = self.GetInteractor()
1215        self.end_x, self.end_y = rwi.GetEventPosition()
1216        self.DrawRubberBand(self.start_x, self.end_x, self.start_y, self.end_y)
def InitializeScreenDrawing(self):
1218    def InitializeScreenDrawing(self):
1219        # make an image of the currently rendered image
1220
1221        rwi = self.GetInteractor()
1222        rwin = rwi.GetRenderWindow()
1223
1224        size = rwin.GetSize()
1225
1226        self._pixel_array.Initialize()
1227        self._pixel_array.SetNumberOfComponents(4)
1228        self._pixel_array.SetNumberOfTuples(size[0] * size[1])
1229
1230        front = 1  # what does this do?
1231        rwin.GetRGBACharPixelData(0, 0, size[0] - 1, size[1] - 1, front, self._pixel_array)
def DrawRubberBand(self, x1, x2, y1, y2):
1233    def DrawRubberBand(self, x1, x2, y1, y2):
1234        rwi = self.GetInteractor()
1235        rwin = rwi.GetRenderWindow()
1236
1237        size = rwin.GetSize()
1238
1239        tempPA = vtk.vtkUnsignedCharArray()
1240        tempPA.DeepCopy(self._pixel_array)
1241
1242        # check size, viewport may have been resized in the mean-time
1243        if tempPA.GetNumberOfTuples() != size[0] * size[1]:
1244            # print(
1245            #     "Starting new screen-image - viewport has resized without us knowing"
1246            # )
1247            self.InitializeScreenDrawing()
1248            self.DrawRubberBand(x1, x2, y1, y2)
1249            return
1250
1251        x2 = min(x2, size[0] - 1)
1252        y2 = min(y2, size[1] - 1)
1253
1254        x2 = max(x2, 0)
1255        y2 = max(y2, 0)
1256
1257        # Modify the pixel array
1258        width = abs(x2 - x1)
1259        height = abs(y2 - y1)
1260        minx = min(x2, x1)
1261        miny = min(y2, y1)
1262
1263        # draw top and bottom
1264        for i in range(width):
1265
1266            # c = round((10*i % 254)/254) * 254  # find some alternating color
1267            c = 0
1268
1269            idx = (miny * size[0]) + minx + i
1270            tempPA.SetTuple(idx, (c, c, c, 1))
1271
1272            idx = ((miny + height) * size[0]) + minx + i
1273            tempPA.SetTuple(idx, (c, c, c, 1))
1274
1275        # draw left and right
1276        for i in range(height):
1277            # c = round((10 * i % 254) / 254) * 254  # find some alternating color
1278            c = 0
1279
1280            idx = ((miny + i) * size[0]) + minx
1281            tempPA.SetTuple(idx, (c, c, c, 1))
1282
1283            idx = idx + width
1284            tempPA.SetTuple(idx, (c, c, c, 1))
1285
1286        # and Copy back to the window
1287        rwin.SetRGBACharPixelData(0, 0, size[0] - 1, size[1] - 1, tempPA, 0)
1288        rwin.Frame()
def LineToPixels(self, x1, x2, y1, y2):
1290    def LineToPixels(self, x1, x2, y1, y2):
1291        """Returns the x and y values of the pixels on a line between x1,y1 and x2,y2.
1292        If start and end are identical then a single point is returned"""
1293
1294        dx = x2 - x1
1295        dy = y2 - y1
1296
1297        if dx == 0 and dy == 0:
1298            return [x1], [y1]
1299
1300        if abs(dx) > abs(dy):
1301            dhdw = dy / dx
1302            r = range(0, dx, int(dx / abs(dx)))
1303            x = [x1 + i for i in r]
1304            y = [round(y1 + dhdw * i) for i in r]
1305        else:
1306            dwdh = dx / dy
1307            r = range(0, dy, int(dy / abs(dy)))
1308            y = [y1 + i for i in r]
1309            x = [round(x1 + i * dwdh) for i in r]
1310
1311        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 DrawLine(self, x1, x2, y1, y2):
1313    def DrawLine(self, x1, x2, y1, y2):
1314        rwi = self.GetInteractor()
1315        rwin = rwi.GetRenderWindow()
1316
1317        size = rwin.GetSize()
1318
1319        x1 = min(max(x1, 0), size[0])
1320        x2 = min(max(x2, 0), size[0])
1321        y1 = min(max(y1, 0), size[1])
1322        y2 = min(max(y2, 0), size[1])
1323
1324        tempPA = vtk.vtkUnsignedCharArray()
1325        tempPA.DeepCopy(self._pixel_array)
1326
1327        xs, ys = self.LineToPixels(x1, x2, y1, y2)
1328        for x, y in zip(xs, ys):
1329            idx = (y * size[0]) + x
1330            tempPA.SetTuple(idx, (0, 0, 0, 1))
1331
1332        # and Copy back to the window
1333        rwin.SetRGBACharPixelData(0, 0, size[0] - 1, size[1] - 1, tempPA, 0)
1334
1335        camera = self.GetCurrentRenderer().GetActiveCamera()
1336        scale = camera.GetParallelScale()
1337
1338        # Set/Get the scaling used for a parallel projection, i.e.
1339        #
1340        # the half of the height of the viewport in world-coordinate distances.
1341        # The default is 1. Note that the "scale" parameter works as an "inverse scale"
1342        #  larger numbers produce smaller images.
1343        # This method has no effect in perspective projection mode
1344
1345        half_height = size[1] / 2
1346        # half_height [px] = scale [world-coordinates]
1347
1348        length = ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5
1349        meters_per_pixel = scale / half_height
1350        meters = length * meters_per_pixel
1351
1352        if camera.GetParallelProjection():
1353            print(f"Line length = {length} px = {meters} m")
1354        else:
1355            print("Need to be in non-perspective mode to measure. Press 2 or 3 to get there")
1356
1357        if self.callbackMeasure:
1358            self.callbackMeasure(meters)
1359
1360        #
1361        # # can we add something to the window here?
1362        # freeType = vtk.vtkFreeTypeTools.GetInstance()
1363        # textProperty = vtk.vtkTextProperty()
1364        # textProperty.SetJustificationToLeft()
1365        # textProperty.SetFontSize(24)
1366        # textProperty.SetOrientation(25)
1367        #
1368        # textImage = vtk.vtkImageData()
1369        # freeType.RenderString(textProperty, "a somewhat longer text", 72, textImage)
1370        # # this does not give an error, assume it works
1371        # #
1372        # textImage.GetDimensions()
1373        # textImage.GetExtent()
1374        #
1375        # # # Now put the textImage in the RenderWindow
1376        # rwin.SetRGBACharPixelData(0, 0, size[0] - 1, size[1] - 1, textImage, 0)
1377
1378        rwin.Frame()
def UpdateMiddleMouseButtonLockActor(self):
1380    def UpdateMiddleMouseButtonLockActor(self):
1381
1382        if self.middle_mouse_lock_actor is None:
1383            # create the actor
1384            # Create a text on the top-rightcenter
1385            textMapper = vtk.vtkTextMapper()
1386            textMapper.SetInput("Middle mouse lock [m or space] active")
1387            textProp = textMapper.GetTextProperty()
1388            textProp.SetFontSize(12)
1389            textProp.SetFontFamilyToTimes()
1390            textProp.BoldOff()
1391            textProp.ItalicOff()
1392            textProp.ShadowOff()
1393            textProp.SetVerticalJustificationToTop()
1394            textProp.SetJustificationToCentered()
1395            textProp.SetColor((0, 0, 0))
1396
1397            self.middle_mouse_lock_actor = vtk.vtkActor2D()
1398            self.middle_mouse_lock_actor.SetMapper(textMapper)
1399            self.middle_mouse_lock_actor.GetPositionCoordinate().SetCoordinateSystemToNormalizedDisplay()
1400            self.middle_mouse_lock_actor.GetPositionCoordinate().SetValue(0.5, 0.98)
1401
1402            self.GetCurrentRenderer().AddActor(self.middle_mouse_lock_actor)
1403
1404        self.middle_mouse_lock_actor.SetVisibility(self.middle_mouse_lock)
1405        self.DoRender()
def DoRender(self):
1407    def DoRender(self):
1408        self.GetInteractor().Render()