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

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

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

Interaction:

Left button: Sections

Left button: select

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

Middle button: Navigation

Middle button: rotate

Middle button + shift : pan

Middle button + ctrl : zoom

Middle button + alt : center view on picked point

OR

Middle button + alt : zoom rubber band

Mouse wheel : zoom

Right button : context

Right key click: reserved for context-menu

Keys

2 or 3 : toggle perspective view

a : zoom all

x,y,z : view direction (toggles positive and negative)

left/right arrows: rotate 45 deg clockwise/ccw about z-axis, snaps to nearest 45 deg b : box zoom

m : mouse middle lock (toggles)

space : same as middle mouse button

g : grab (move actors)

enter : accept drag

esc : cancel drag, call callbackEscape

LAPTOP MODE

Use space or m as replacement for middle button (m is sticky, space is not)

callbacks / overriding keys:

if callback_any_key is assigned then this function is called on every key press. If this function returns True then further processing of events is stopped.

Moving actors

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

When 'g' is pressed (grab) then a _BlenderStyleDragInfo dataclass object is assigned to style to style.draginfo.

_BlenderStyleDragInfo includes a list of all the actors that are being dragged. By default this is the selection, but this may be altered. Drag is accepted using enter, click, or g. Drag is cancelled by esc

Events

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

callback_end_drag is called when the drag is accepted.

Responding to other events

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

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

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

Rotates CW or CCW to the nearest 45 deg angle

  • includes some fuzzyness to determine about which axis
def ToggleParallelProjection(self):
683    def ToggleParallelProjection(self):
684        renderer = self.GetCurrentRenderer()
685        camera = renderer.GetActiveCamera()
686        camera.SetParallelProjection(not bool(camera.GetParallelProjection()))
687        self.GetInteractor().Render()
def SetViewX(self):
689    def SetViewX(self):
690        self.SetCameraPlaneDirection((1, 0, 0))
def SetViewY(self):
692    def SetViewY(self):
693        self.SetCameraPlaneDirection((0, 1, 0))
def SetViewZ(self):
695    def SetViewZ(self):
696        self.SetCameraPlaneDirection((0, 0, 1))
def ZoomFit(self):
698    def ZoomFit(self):
699        self.GetCurrentRenderer().ResetCamera()
700        self.GetInteractor().Render()
def SetCameraPlaneDirection(self, direction):
702    def SetCameraPlaneDirection(self, direction):
703        """Sets the camera to display a plane of which direction is the normal
704        - includes logic to reverse the direction if benificial"""
705
706        CurrentRenderer = self.GetCurrentRenderer()
707        camera = CurrentRenderer.GetActiveCamera()
708
709        direction = np.array(direction)
710
711        normal = camera.GetViewPlaneNormal()
712        # can not set the normal, need to change the position to do that
713
714        current_alignment = np.dot(normal, -direction)
715        # print(f"Current alignment = {current_alignment}")
716
717        if abs(current_alignment) > 0.9999:
718            # print("toggling")
719            direction = -np.array(normal)
720        elif current_alignment > 0:  # find the nearest plane
721            # print("reversing to find nearest")
722            direction = -direction
723
724        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):
726    def SetCameraDirection(self, direction):
727        """Sets the camera to this direction, sets view up if horizontal enough"""
728        direction = np.array(direction)
729
730        CurrentRenderer = self.GetCurrentRenderer()
731        camera = CurrentRenderer.GetActiveCamera()
732        rwi = self.GetInteractor()
733
734        pos = np.array(camera.GetPosition())
735        focal = np.array(camera.GetFocalPoint())
736        dist = np.linalg.norm(pos - focal)
737
738        pos = focal - dist * direction
739        camera.SetPosition(pos)
740
741        if abs(direction[2]) < 0.9:
742            camera.SetViewUp(0, 0, 1)
743        elif direction[2] > 0.9:
744            camera.SetViewUp(0, -1, 0)
745        else:
746            camera.SetViewUp(0, 1, 0)
747
748        camera.OrthogonalizeViewUp()
749
750        if self.GetAutoAdjustCameraClippingRange():
751            CurrentRenderer.ResetCameraClippingRange()
752
753        if rwi.GetLightFollowCamera():
754            CurrentRenderer.UpdateLightsGeometryToFollowCamera()
755
756        if self.callback_camera_direction_changed:
757            self.callback_camera_direction_changed()
758
759        self.GetInteractor().Render()

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

def PerformPickingOnSelection(self):
761    def PerformPickingOnSelection(self):
762        """Preforms prop3d picking on the current dragged selection
763
764        If the distance between the start and endpoints is less than the threshold
765        then a SINGLE prop3d is picked along the line
766
767        the selection area is drawn by the rubber band and is defined by
768        self.start_x, self.start_y, self.end_x, self.end_y
769        """
770        renderer = self.GetCurrentRenderer()
771
772        assemblyPath = renderer.PickProp(self.start_x, self.start_y, self.end_x, self.end_y)
773
774        # re-pick in larger area if nothing is returned
775        if not assemblyPath:
776            self.start_x -= 2
777            self.end_x += 2
778            self.start_y -= 2
779            self.end_y += 2
780            assemblyPath = renderer.PickProp(self.start_x, self.start_y, self.end_x, self.end_y)
781
782        # The nearest prop (by Z-value)
783        if assemblyPath:
784            assert (
785                assemblyPath.GetNumberOfItems() == 1
786            ), "Wrong assumption on number of returned nodes when picking"
787            nearest_prop = assemblyPath.GetItemAsObject(0).GetViewProp()
788
789            # all props
790            collection = renderer.GetPickResultProps()
791            props = [collection.GetItemAsObject(i) for i in range(collection.GetNumberOfItems())]
792
793            props.remove(nearest_prop)
794            props.insert(0, nearest_prop)
795
796            return props
797
798        else:
799            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):
803    def StartDrag(self):
804        if self.callback_start_drag:
805            # print("Calling callback_start_drag")
806            self.callback_start_drag()
807            return
808        else:  # grab the current selection
809            if self.picked_props:
810                self.StartDragOnProps(self.picked_props)
811            else:
812                pass
813                # print('Can not start drag, nothing selected and callback_start_drag not assigned')
def FinishDrag(self):
815    def FinishDrag(self):
816        # print('Finished drag')
817        if self.callback_end_drag:
818            # reset actor positions as actors positions will be controlled by called functions
819            for pos0, actor in zip(
820                self.draginfo.dragged_actors_original_positions, self.draginfo.actors_dragging
821            ):
822                actor.SetPosition(pos0)
823            self.callback_end_drag(self.draginfo)
824
825        self.draginfo = None
def StartDragOnProps(self, props):
827    def StartDragOnProps(self, props):
828        """Starts drag on the provided props (actors) by filling self.draginfo"""
829        if self.draginfo is not None:
830            self.FinishDrag()
831            return
832
833        # print('Starting drag')
834
835        # create and fill drag-info
836        draginfo = _BlenderStyleDragInfo()
837
838        #
839        # draginfo.dragged_node = node
840        #
841        # # find all actors and outlines corresponding to this node
842        # actors = [*self.actor_from_node(node).actors.values()]
843        # outlines = [ol.outline_actor for ol in self.node_outlines if ol.parent_vp_actor in actors]
844
845        draginfo.actors_dragging = props  # [*actors, *outlines]
846
847        for a in draginfo.actors_dragging:
848            draginfo.dragged_actors_original_positions.append(a.GetPosition())  # numpy ndarray
849
850        # Get the start position of the drag in 3d
851
852        rwi = self.GetInteractor()
853        CurrentRenderer = self.GetCurrentRenderer()
854        camera = CurrentRenderer.GetActiveCamera()
855        viewFocus = camera.GetFocalPoint()
856
857        temp_out = [0, 0, 0]
858        self.ComputeWorldToDisplay(
859            CurrentRenderer, viewFocus[0], viewFocus[1], viewFocus[2], temp_out
860        )
861        focalDepth = temp_out[2]
862
863        newPickPoint = [0, 0, 0, 0]
864        x, y = rwi.GetEventPosition()
865        self.ComputeDisplayToWorld(CurrentRenderer, x, y, focalDepth, newPickPoint)
866
867        mouse_pos_3d = np.array(newPickPoint[:3])
868
869        draginfo.start_position_3d = mouse_pos_3d
870
871        self.draginfo = draginfo

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

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

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

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

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

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

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

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

Zooms to a box

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

Move the camera to focus on this particular prop3D

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

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

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