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