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