vedo.addons
Create additional objects like axes, legends, lights, etc.
1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3import numpy as np 4from typing import Union 5from typing_extensions import Self 6 7import vedo.vtkclasses as vtki # a wrapper for lazy imports 8 9import vedo 10from vedo import settings 11from vedo import utils 12from vedo import shapes 13from vedo.transformations import LinearTransform 14from vedo.assembly import Assembly, Group 15from vedo.colors import get_color, build_lut, color_map, printc 16from vedo.mesh import Mesh 17from vedo.pointcloud import Points, Point, merge 18from vedo.grids import TetMesh 19from vedo.volume import Volume 20 21__docformat__ = "google" 22 23__doc__ = """ 24Create additional objects like axes, legends, lights, etc. 25 26![](https://vedo.embl.es/images/pyplot/customAxes2.png) 27""" 28 29__all__ = [ 30 "ScalarBar", 31 "ScalarBar3D", 32 "Slider2D", 33 "Slider3D", 34 "Icon", 35 "LegendBox", 36 "Light", 37 "Axes", 38 "RendererFrame", 39 "Ruler2D", 40 "Ruler3D", 41 "RulerAxes", 42 "DistanceTool", 43 "SplineTool", 44 "DrawingWidget", 45 "Goniometer", 46 "Button", 47 "ButtonWidget", 48 "Flagpost", 49 "ProgressBarWidget", 50 "BoxCutter", 51 "PlaneCutter", 52 "SphereCutter", 53] 54 55######################################################################################## 56class Flagpost(vtki.vtkFlagpoleLabel): 57 """ 58 Create a flag post style element to describe an object. 59 """ 60 61 def __init__( 62 self, 63 txt="", 64 base=(0, 0, 0), 65 top=(0, 0, 1), 66 s=1, 67 c="k9", 68 bc="k1", 69 alpha=1, 70 lw=0, 71 font="Calco", 72 justify="center-left", 73 vspacing=1, 74 ): 75 """ 76 Create a flag post style element to describe an object. 77 78 Arguments: 79 txt : (str) 80 Text to display. The default is the filename or the object name. 81 base : (list) 82 position of the flag anchor point. 83 top : (list) 84 a 3D displacement or offset. 85 s : (float) 86 size of the text to be shown 87 c : (list) 88 color of text and line 89 bc : (list) 90 color of the flag background 91 alpha : (float) 92 opacity of text and box. 93 lw : (int) 94 line with of box frame. The default is 0. 95 font : (str) 96 font name. Use a monospace font for better rendering. The default is "Calco". 97 Type `vedo -r fonts` for a font demo. 98 Check [available fonts here](https://vedo.embl.es/fonts). 99 justify : (str) 100 internal text justification. The default is "center-left". 101 vspacing : (float) 102 vertical spacing between lines. 103 104 Examples: 105 - [flag_labels2.py](https://github.com/marcomusy/vedo/tree/master/examples/examples/other/flag_labels2.py) 106 107 ![](https://vedo.embl.es/images/other/flag_labels2.png) 108 """ 109 110 super().__init__() 111 112 base = utils.make3d(base) 113 top = utils.make3d(top) 114 115 self.SetBasePosition(*base) 116 self.SetTopPosition(*top) 117 118 self.SetFlagSize(s) 119 self.SetInput(txt) 120 self.PickableOff() 121 122 self.GetProperty().LightingOff() 123 self.GetProperty().SetLineWidth(lw + 1) 124 125 prop = self.GetTextProperty() 126 if bc is not None: 127 prop.SetBackgroundColor(get_color(bc)) 128 129 prop.SetOpacity(alpha) 130 prop.SetBackgroundOpacity(alpha) 131 if bc is not None and len(bc) == 4: 132 prop.SetBackgroundRGBA(alpha) 133 134 c = get_color(c) 135 prop.SetColor(c) 136 self.GetProperty().SetColor(c) 137 138 prop.SetFrame(bool(lw)) 139 prop.SetFrameWidth(lw) 140 prop.SetFrameColor(prop.GetColor()) 141 142 prop.SetFontFamily(vtki.VTK_FONT_FILE) 143 fl = utils.get_font_path(font) 144 prop.SetFontFile(fl) 145 prop.ShadowOff() 146 prop.BoldOff() 147 prop.SetOpacity(alpha) 148 prop.SetJustificationToLeft() 149 if "top" in justify: 150 prop.SetVerticalJustificationToTop() 151 if "bottom" in justify: 152 prop.SetVerticalJustificationToBottom() 153 if "cent" in justify: 154 prop.SetVerticalJustificationToCentered() 155 prop.SetJustificationToCentered() 156 if "left" in justify: 157 prop.SetJustificationToLeft() 158 if "right" in justify: 159 prop.SetJustificationToRight() 160 prop.SetLineSpacing(vspacing * 1.2) 161 self.SetUseBounds(False) 162 163 def text(self, value: str) -> Self: 164 self.SetInput(value) 165 return self 166 167 def on(self) -> Self: 168 self.VisibilityOn() 169 return self 170 171 def off(self) -> Self: 172 self.VisibilityOff() 173 return self 174 175 def toggle(self) -> Self: 176 self.SetVisibility(not self.GetVisibility()) 177 return self 178 179 def use_bounds(self, value=True) -> Self: 180 self.SetUseBounds(value) 181 return self 182 183 def color(self, c) -> Self: 184 c = get_color(c) 185 self.GetTextProperty().SetColor(c) 186 self.GetProperty().SetColor(c) 187 return self 188 189 def pos(self, p) -> Self: 190 p = np.asarray(p) 191 self.top = self.top - self.base + p 192 self.base = p 193 return self 194 195 @property 196 def base(self) -> np.ndarray: 197 return np.array(self.GetBasePosition()) 198 199 @base.setter 200 def base(self, value): 201 self.SetBasePosition(*value) 202 203 @property 204 def top(self) -> np.ndarray: 205 return np.array(self.GetTopPosition()) 206 207 @top.setter 208 def top(self, value): 209 self.SetTopPosition(*value) 210 211 212########################################################################################### 213class LegendBox(shapes.TextBase, vtki.vtkLegendBoxActor): 214 """ 215 Create a 2D legend box. 216 """ 217 218 def __init__( 219 self, 220 entries=(), 221 nmax=12, 222 c=None, 223 font="", 224 width=0.18, 225 height=None, 226 padding=2, 227 bg="k8", 228 alpha=0.25, 229 pos="top-right", 230 markers=None, 231 ): 232 """ 233 Create a 2D legend box for the list of specified objects. 234 235 Arguments: 236 nmax : (int) 237 max number of legend entries 238 c : (color) 239 text color, leave as None to pick the mesh color automatically 240 font : (str) 241 Check [available fonts here](https://vedo.embl.es/fonts) 242 width : (float) 243 width of the box as fraction of the window width 244 height : (float) 245 height of the box as fraction of the window height 246 padding : (int) 247 padding space in units of pixels 248 bg : (color) 249 background color of the box 250 alpha: (float) 251 opacity of the box 252 pos : (str, list) 253 position of the box, can be either a string or a (x,y) screen position in range [0,1] 254 255 Examples: 256 - [legendbox.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/legendbox.py) 257 - [flag_labels1.py](https://github.com/marcomusy/vedo/tree/master/examples/other/flag_labels1.py) 258 - [flag_labels2.py](https://github.com/marcomusy/vedo/tree/master/examples/other/flag_labels2.py) 259 260 ![](https://vedo.embl.es/images/other/flag_labels.png) 261 """ 262 super().__init__() 263 264 self.name = "LegendBox" 265 self.entries = entries[:nmax] 266 self.properties = self.GetEntryTextProperty() 267 268 n = 0 269 texts = [] 270 for e in self.entries: 271 ename = e.name 272 if "legend" in e.info.keys(): 273 if not e.info["legend"]: 274 ename = "" 275 else: 276 ename = str(e.info["legend"]) 277 if ename: 278 n += 1 279 texts.append(ename) 280 self.SetNumberOfEntries(n) 281 282 if not n: 283 return 284 285 self.ScalarVisibilityOff() 286 self.PickableOff() 287 self.SetPadding(padding) 288 289 self.properties.ShadowOff() 290 self.properties.BoldOff() 291 292 # self.properties.SetJustificationToLeft() # no effect 293 # self.properties.SetVerticalJustificationToTop() 294 295 if not font: 296 font = settings.default_font 297 298 self.font(font) 299 300 n = 0 301 for i in range(len(self.entries)): 302 ti = texts[i] 303 if not ti: 304 continue 305 e = entries[i] 306 if c is None: 307 col = e.properties.GetColor() 308 if col == (1, 1, 1): 309 col = (0.2, 0.2, 0.2) 310 else: 311 col = get_color(c) 312 if markers is None: # default 313 poly = e.dataset 314 else: 315 marker = markers[i] if utils.is_sequence(markers) else markers 316 if isinstance(marker, Points): 317 poly = marker.clone(deep=False).normalize().shift(0, 1, 0).dataset 318 else: # assume string marker 319 poly = vedo.shapes.Marker(marker, s=1).shift(0, 1, 0).dataset 320 321 self.SetEntry(n, poly, ti, col) 322 n += 1 323 324 self.SetWidth(width) 325 if height is None: 326 self.SetHeight(width / 3.0 * n) 327 else: 328 self.SetHeight(height) 329 330 self.pos(pos) 331 332 if alpha: 333 self.UseBackgroundOn() 334 self.SetBackgroundColor(get_color(bg)) 335 self.SetBackgroundOpacity(alpha) 336 else: 337 self.UseBackgroundOff() 338 self.LockBorderOn() 339 340 @property 341 def width(self): 342 """Return the width of the legend box.""" 343 return self.GetWidth() 344 345 @property 346 def height(self): 347 """Return the height of the legend box.""" 348 return self.GetHeight() 349 350 def pos(self, pos): 351 """Set the position of the legend box.""" 352 sx, sy = 1 - self.GetWidth(), 1 - self.GetHeight() 353 if pos == 1 or ("top" in pos and "left" in pos): 354 self.GetPositionCoordinate().SetValue(0, sy) 355 elif pos == 2 or ("top" in pos and "right" in pos): 356 self.GetPositionCoordinate().SetValue(sx, sy) 357 elif pos == 3 or ("bottom" in pos and "left" in pos): 358 self.GetPositionCoordinate().SetValue(0, 0) 359 elif pos == 4 or ("bottom" in pos and "right" in pos): 360 self.GetPositionCoordinate().SetValue(sx, 0) 361 elif "cent" in pos and "right" in pos: 362 self.GetPositionCoordinate().SetValue(sx, sy - 0.25) 363 elif "cent" in pos and "left" in pos: 364 self.GetPositionCoordinate().SetValue(0, sy - 0.25) 365 elif "cent" in pos and "bottom" in pos: 366 self.GetPositionCoordinate().SetValue(sx - 0.25, 0) 367 elif "cent" in pos and "top" in pos: 368 self.GetPositionCoordinate().SetValue(sx - 0.25, sy) 369 elif utils.is_sequence(pos): 370 self.GetPositionCoordinate().SetValue(pos[0], pos[1]) 371 else: 372 vedo.logger.error("LegendBox: pos must be in range [1-4] or a [x,y] list") 373 374 return self 375 376 377class ButtonWidget: 378 """ 379 Create a button widget. 380 """ 381 382 def __init__( 383 self, 384 function, 385 states=(), 386 c=("white"), 387 bc=("green4"), 388 alpha=1.0, 389 font="Calco", 390 size=100, 391 plotter=None, 392 ): 393 """ 394 Create a button widget. 395 396 States can be either text strings or images. 397 398 Arguments: 399 function : (function) 400 external function to be called by the widget 401 states : (list) 402 the list of possible states, eg. ['On', 'Off'] 403 c : (list) 404 the list of colors for each state eg. ['red3', 'green5'] 405 bc : (list) 406 the list of background colors for each state 407 alpha : (float) 408 opacity level 409 font : (str) 410 font type 411 size : (int) 412 size of button font 413 plotter : (Plotter) 414 the plotter object to which the widget is added 415 416 Example: 417 ```py 418 from vedo import * 419 420 def button_func(widget, evtname): 421 print("button_func called") 422 cone.color(button.state) 423 424 def on_mouse_click(event): 425 if event.object: 426 print("on_mouse_click", event) 427 cone.color(button.state) 428 429 # Create a cone 430 cone = Cone().color(0) 431 432 # Create a plotter 433 plt = Plotter(bg='bb', axes=1) 434 plt.add_callback('mouse click', on_mouse_click) 435 436 plt.add(cone) 437 438 # Create a button widget 439 img0 = Image("play-button.png") 440 img1 = Image("power-on.png") 441 442 button = ButtonWidget( 443 button_func, 444 # states=["State 0", "State 1"], 445 states=[img0, img1], 446 c=["red4", "blue4"], 447 bc=("k9", "k5"), 448 size=100, 449 plotter=plt, 450 ) 451 button.pos([0,0]).enable() 452 453 plt.show() 454 ``` 455 """ 456 457 self.widget = vtki.new("ButtonWidget") 458 459 self.function = function 460 self.states = states 461 self.colors = c 462 self.background_colors = bc 463 self.plotter = plotter 464 self.size = size 465 466 assert len(states) == len(c), "states and colors must have the same length" 467 assert len(states) == len(bc), "states and background colors must have the same length" 468 469 self.interactor = None 470 if plotter is not None: 471 self.interactor = plotter.interactor 472 self.widget.SetInteractor(plotter.interactor) 473 else: 474 if vedo.plotter_instance: 475 self.interactor = vedo.plotter_instance.interactor 476 self.widget.SetInteractor(self.interactor) 477 478 self.representation = vtki.new("TexturedButtonRepresentation2D") 479 self.representation.SetNumberOfStates(len(states)) 480 for i, state in enumerate(states): 481 482 if isinstance(state, vedo.Image): 483 state = state.dataset 484 485 elif isinstance(state, str): 486 txt = state 487 tp = vtki.vtkTextProperty() 488 tp.BoldOff() 489 tp.FrameOff() 490 col = c[i] 491 tp.SetColor(vedo.get_color(col)) 492 tp.ShadowOff() 493 tp.ItalicOff() 494 col = bc[i] 495 tp.SetBackgroundColor(vedo.get_color(col)) 496 tp.SetBackgroundOpacity(alpha) 497 tp.UseTightBoundingBoxOff() 498 499 # tp.SetJustificationToLeft() 500 # tp.SetVerticalJustificationToCentered() 501 # tp.SetJustificationToCentered() 502 width, height = 100 * len(txt), 1000 503 504 fpath = vedo.utils.get_font_path(font) 505 tp.SetFontFamily(vtki.VTK_FONT_FILE) 506 tp.SetFontFile(fpath) 507 508 tr = vtki.new("TextRenderer") 509 fs = tr.GetConstrainedFontSize(txt, tp, width, height, 500) 510 tp.SetFontSize(fs) 511 512 img = vtki.vtkImageData() 513 tr.RenderString(tp, txt, img, [width, height], 500) 514 state = img 515 516 self.representation.SetButtonTexture(i, state) 517 518 self.widget.SetRepresentation(self.representation) 519 self.widget.AddObserver("StateChangedEvent", function) 520 521 def __del__(self): 522 self.widget.Off() 523 self.widget.SetInteractor(None) 524 self.widget.SetRepresentation(None) 525 self.representation = None 526 self.interactor = None 527 self.function = None 528 self.states = () 529 self.widget = None 530 self.plotter = None 531 532 def pos(self, pos): 533 assert len(pos) == 2, "pos must be a 2D position" 534 if not self.plotter: 535 vedo.logger.warning("ButtonWidget: pos() can only be used if a Plotter is provided") 536 return self 537 coords = vtki.vtkCoordinate() 538 coords.SetCoordinateSystemToNormalizedDisplay() 539 coords.SetValue(pos[0], pos[1]) 540 sz = self.size 541 ren = self.plotter.renderer 542 p = coords.GetComputedDisplayValue(ren) 543 bds = [0, 0, 0, 0, 0, 0] 544 bds[0] = p[0] - sz 545 bds[1] = bds[0] + sz 546 bds[2] = p[1] - sz 547 bds[3] = bds[2] + sz 548 self.representation.SetPlaceFactor(1) 549 self.representation.PlaceWidget(bds) 550 return self 551 552 def enable(self): 553 self.widget.On() 554 return self 555 556 def disable(self): 557 self.widget.Off() 558 return self 559 560 def next_state(self): 561 self.representation.NextState() 562 return self 563 564 @property 565 def state(self): 566 return self.representation.GetState() 567 568 @state.setter 569 def state(self, i): 570 self.representation.SetState(i) 571 572 573class Button(vedo.shapes.Text2D): 574 """ 575 Build a Button object. 576 """ 577 578 def __init__( 579 self, 580 fnc=None, 581 states=("Button"), 582 c=("white"), 583 bc=("green4"), 584 pos=(0.7, 0.1), 585 size=24, 586 font="Courier", 587 bold=True, 588 italic=False, 589 alpha=1, 590 angle=0, 591 ): 592 """ 593 Build a Button object to be shown in the rendering window. 594 595 Arguments: 596 fnc : (function) 597 external function to be called by the widget 598 states : (list) 599 the list of possible states, eg. ['On', 'Off'] 600 c : (list) 601 the list of colors for each state eg. ['red3', 'green5'] 602 bc : (list) 603 the list of background colors for each state 604 pos : (list, str) 605 2D position in pixels from left-bottom corner 606 size : (int) 607 size of button font 608 font : (str) 609 font type 610 bold : (bool) 611 set bold font face 612 italic : (bool) 613 italic font face 614 alpha : (float) 615 opacity level 616 angle : (float) 617 anticlockwise rotation in degrees 618 619 Examples: 620 - [buttons1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/buttons1.py) 621 - [buttons2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/buttons2.py) 622 623 ![](https://user-images.githubusercontent.com/32848391/50738870-c0fe2500-11d8-11e9-9b78-92754f5c5968.jpg) 624 625 - [timer_callback2.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/timer_callback2.py) 626 627 ![](https://vedo.embl.es/images/advanced/timer_callback1.jpg) 628 """ 629 super().__init__() 630 631 self.status_idx = 0 632 633 self.spacer = " " 634 635 self.states = states 636 637 if not utils.is_sequence(c): 638 c = [c] 639 self.colors = c 640 641 if not utils.is_sequence(bc): 642 bc = [bc] 643 self.bcolors = bc 644 645 assert len(c) == len(bc), "in Button color number mismatch!" 646 647 self.function = fnc 648 self.function_id = None 649 650 self.status(0) 651 652 if font == "courier": 653 font = font.capitalize() 654 self.font(font).bold(bold).italic(italic) 655 656 self.alpha(alpha).angle(angle) 657 self.size(size / 20) 658 self.pos(pos, "center") 659 self.PickableOn() 660 661 def status(self, s=None) -> "Button": 662 """ 663 Set/Get the status of the button. 664 """ 665 if s is None: 666 return self.states[self.status_idx] 667 668 if isinstance(s, str): 669 s = self.states.index(s) 670 self.status_idx = s 671 self.text(self.spacer + self.states[s] + self.spacer) 672 s = s % len(self.bcolors) 673 self.color(self.colors[s]) 674 self.background(self.bcolors[s]) 675 return self 676 677 def switch(self) -> "Button": 678 """ 679 Change/cycle button status to the next defined status in states list. 680 """ 681 self.status_idx = (self.status_idx + 1) % len(self.states) 682 self.status(self.status_idx) 683 return self 684 685 686##################################################################### 687class SplineTool(vtki.vtkContourWidget): 688 """ 689 Spline tool, draw a spline through a set of points interactively. 690 """ 691 692 def __init__( 693 self, 694 points, 695 pc="k", 696 ps=8, 697 lc="r4", 698 ac="g5", 699 lw=2, 700 alpha=1, 701 closed=False, 702 ontop=True, 703 can_add_nodes=True, 704 ): 705 """ 706 Spline tool, draw a spline through a set of points interactively. 707 708 Arguments: 709 points : (list), Points 710 initial set of points. 711 pc : (str) 712 point color. 713 ps : (int) 714 point size. 715 lc : (str) 716 line color. 717 ac : (str) 718 active point color. 719 lw : (int) 720 line width. 721 alpha : (float) 722 line transparency level. 723 closed : (bool) 724 spline is closed or open. 725 ontop : (bool) 726 show it always on top of other objects. 727 can_add_nodes : (bool) 728 allow to add (or remove) new nodes interactively. 729 730 Examples: 731 - [spline_tool.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/spline_tool.py) 732 733 ![](https://vedo.embl.es/images/basic/spline_tool.png) 734 """ 735 super().__init__() 736 737 self.representation = self.GetRepresentation() 738 self.representation.SetAlwaysOnTop(ontop) 739 self.SetAllowNodePicking(can_add_nodes) 740 741 self.representation.GetLinesProperty().SetColor(get_color(lc)) 742 self.representation.GetLinesProperty().SetLineWidth(lw) 743 self.representation.GetLinesProperty().SetOpacity(alpha) 744 if lw == 0 or alpha == 0: 745 self.representation.GetLinesProperty().SetOpacity(0) 746 747 self.representation.GetActiveProperty().SetLineWidth(lw + 1) 748 self.representation.GetActiveProperty().SetColor(get_color(ac)) 749 750 self.representation.GetProperty().SetColor(get_color(pc)) 751 self.representation.GetProperty().SetPointSize(ps) 752 self.representation.GetProperty().RenderPointsAsSpheresOn() 753 754 # self.representation.BuildRepresentation() # crashes 755 756 self.SetRepresentation(self.representation) 757 758 if utils.is_sequence(points): 759 self.points = Points(points) 760 else: 761 self.points = points 762 763 self.closed = closed 764 765 @property 766 def interactor(self): 767 """Return the current interactor.""" 768 return self.GetInteractor() 769 770 @interactor.setter 771 def interactor(self, iren): 772 """Set the current interactor.""" 773 self.SetInteractor(iren) 774 775 def add(self, pt) -> "SplineTool": 776 """ 777 Add one point at a specified position in space if 3D, 778 or 2D screen-display position if 2D. 779 """ 780 if len(pt) == 2: 781 self.representation.AddNodeAtDisplayPosition(int(pt[0]), int(pt[1])) 782 else: 783 self.representation.AddNodeAtWorldPosition(pt) 784 return self 785 786 def add_observer(self, event, func, priority=1) -> int: 787 """Add an observer to the widget.""" 788 event = utils.get_vtk_name_event(event) 789 cid = self.AddObserver(event, func, priority) 790 return cid 791 792 def remove(self, i: int) -> "SplineTool": 793 """Remove specific node by its index""" 794 self.representation.DeleteNthNode(i) 795 return self 796 797 def on(self) -> "SplineTool": 798 """Activate/Enable the tool""" 799 self.On() 800 self.Render() 801 return self 802 803 def off(self) -> "SplineTool": 804 """Disactivate/Disable the tool""" 805 self.Off() 806 self.Render() 807 return self 808 809 def render(self) -> "SplineTool": 810 """Render the spline""" 811 self.Render() 812 return self 813 814 # def bounds(self) -> np.ndarray: 815 # """Retrieve the bounding box of the spline as [x0,x1, y0,y1, z0,z1]""" 816 # return np.array(self.GetBounds()) 817 818 def spline(self) -> vedo.Line: 819 """Return the vedo.Spline object.""" 820 self.representation.SetClosedLoop(self.closed) 821 self.representation.BuildRepresentation() 822 pd = self.representation.GetContourRepresentationAsPolyData() 823 ln = vedo.Line(pd, lw=2, c="k") 824 return ln 825 826 def nodes(self, onscreen=False) -> np.ndarray: 827 """Return the current position in space (or on 2D screen-display) of the spline nodes.""" 828 n = self.representation.GetNumberOfNodes() 829 pts = [] 830 for i in range(n): 831 p = [0.0, 0.0, 0.0] 832 if onscreen: 833 self.representation.GetNthNodeDisplayPosition(i, p) 834 else: 835 self.representation.GetNthNodeWorldPosition(i, p) 836 pts.append(p) 837 return np.array(pts) 838 839 840class DrawingWidget: 841 def __init__(self, obj, c="green5", lw=4, closed=False, snap_to_image=False): 842 """ 843 3D widget for tracing on planar props. 844 This is primarily designed for manually tracing over image data. 845 846 - Any object can be input rather than just 2D images 847 - The widget fires pick events at the input prop to decide where to move its handles 848 - The widget has 2D glyphs for handles instead of 3D spheres. 849 850 The button actions and key modifiers are as follows for controlling the widget: 851 1) left button click over the image, hold and drag draws a free hand line. 852 2) left button click and release erases the widget line, if it exists, and repositions the first handle. 853 3) middle button click starts a snap drawn line. 854 The line is terminated by clicking the middle button while ressing the ctrl key. 855 4) when tracing a continuous or snap drawn line, if the last cursor position is within a specified 856 tolerance to the first handle, the widget line will form a closed loop. 857 5) right button clicking and holding on any handle that is part of a snap drawn line allows handle dragging: 858 existing line segments are updated accordingly. If the path is open and closing_radius is set, 859 the path can be closed by repositioning the first and last points over one another. 860 6) Ctrl + right button down on any handle will erase it: existing snap drawn line segments are updated accordingly. 861 If the line was formed by continuous tracing, the line is deleted leaving one handle. 862 7) Shift + right button down on any snap drawn line segment will insert a handle at the cursor position. 863 The line segment is split accordingly. 864 865 Arguments: 866 obj : vtkProp 867 The prop to trace on. 868 c : str, optional 869 The color of the line. The default is "green5". 870 lw : int, optional 871 The line width. The default is 4. 872 closed : bool, optional 873 Whether to close the line. The default is False. 874 snap_to_image : bool, optional 875 Whether to snap to the image. The default is False. 876 877 Example: 878 - [spline_draw2.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/spline_draw2.py) 879 """ 880 881 self.widget = vtki.new("ImageTracerWidget") 882 883 self.line = None 884 self.line_properties = self.widget.GetLineProperty() 885 self.line_properties.SetColor(vedo.get_color(c)) 886 self.line_properties.SetLineWidth(lw) 887 self.callback_id = None 888 self.event_name = "EndInteractionEvent" 889 890 if vedo.plotter_instance: 891 self.widget.SetInteractor(vedo.plotter_instance.interactor) 892 if vedo.plotter_instance.renderer: 893 self.widget.SetDefaultRenderer(vedo.plotter_instance.renderer) 894 895 try: 896 self.widget.SetViewProp(obj.actor) 897 except AttributeError: 898 self.widget.SetViewProp(obj) 899 900 if closed: 901 closing_radius = 1e10 902 self.widget.SetAutoClose(1) 903 self.widget.SetCaptureRadius(closing_radius) 904 905 self.widget.SetProjectToPlane(0) 906 self.widget.SetProjectionNormal(2) # XY plane 907 self.widget.SetProjectionPosition(0) 908 self.widget.SetSnapToImage(snap_to_image) 909 910 def callback(self, widget, eventId) -> None: 911 path = vtki.vtkPolyData() 912 widget.GetPath(path) 913 self.line = vedo.shapes.Line(path, c=self.line_properties.GetColor()) 914 # print(f"There are {path.GetNumberOfPoints()} points in the line.") 915 916 def add_observer(self, event, func, priority=1) -> int: 917 """Add an observer to the widget.""" 918 event = utils.get_vtk_name_event(event) 919 cid = self.widget.AddObserver(event, func, priority) 920 return cid 921 922 @property 923 def interactor(self): 924 return self.widget.GetInteractor() 925 926 @interactor.setter 927 def interactor(self, value): 928 self.widget.SetInteractor(value) 929 930 @property 931 def renderer(self): 932 return self.widget.GetDefaultRenderer() 933 934 @renderer.setter 935 def renderer(self, value): 936 self.widget.SetDefaultRenderer(value) 937 938 def on(self) -> Self: 939 self.widget.On() 940 ev_name = vedo.utils.get_vtk_name_event(self.event_name) 941 self.callback_id = self.widget.AddObserver(ev_name, self.callback, 1000) 942 return self 943 944 def off(self) -> None: 945 self.widget.Off() 946 self.widget.RemoveObserver(self.callback_id) 947 948 def freeze(self, value=True) -> Self: 949 self.widget.SetInteraction(not value) 950 return self 951 952 def remove(self) -> None: 953 self.widget.Off() 954 self.widget.RemoveObserver(self.callback_id) 955 self.widget.SetInteractor(None) 956 self.line = None 957 self.line_properties = None 958 self.callback_id = None 959 self.widget = None 960 961 962##################################################################### 963class SliderWidget(vtki.vtkSliderWidget): 964 """Helper class for `vtkSliderWidget`""" 965 966 def __init__(self): 967 super().__init__() 968 969 @property 970 def interactor(self): 971 return self.GetInteractor() 972 973 @interactor.setter 974 def interactor(self, iren): 975 self.SetInteractor(iren) 976 977 @property 978 def representation(self): 979 return self.GetRepresentation() 980 981 @property 982 def value(self): 983 return self.GetRepresentation().GetValue() 984 985 @value.setter 986 def value(self, val): 987 self.GetRepresentation().SetValue(val) 988 989 @property 990 def renderer(self): 991 return self.GetCurrentRenderer() 992 993 @renderer.setter 994 def renderer(self, ren): 995 self.SetCurrentRenderer(ren) 996 997 @property 998 def title(self): 999 self.GetRepresentation().GetTitleText() 1000 1001 @title.setter 1002 def title(self, txt): 1003 self.GetRepresentation().SetTitleText(str(txt)) 1004 1005 @property 1006 def range(self): 1007 xmin = self.GetRepresentation().GetMinimumValue() 1008 xmax = self.GetRepresentation().GetMaximumValue() 1009 return [xmin, xmax] 1010 1011 @range.setter 1012 def range(self, vals): 1013 if vals[0] is not None: 1014 self.GetRepresentation().SetMinimumValue(vals[0]) 1015 if vals[1] is not None: 1016 self.GetRepresentation().SetMaximumValue(vals[1]) 1017 1018 def on(self) -> Self: 1019 self.EnabledOn() 1020 return self 1021 1022 def off(self) -> Self: 1023 self.EnabledOff() 1024 return self 1025 1026 def toggle(self) -> Self: 1027 self.SetEnabled(not self.GetEnabled()) 1028 return self 1029 1030 def add_observer(self, event, func, priority=1) -> int: 1031 """Add an observer to the widget.""" 1032 event = utils.get_vtk_name_event(event) 1033 cid = self.AddObserver(event, func, priority) 1034 return cid 1035 1036 1037##################################################################### 1038def Goniometer( 1039 p1, 1040 p2, 1041 p3, 1042 font="", 1043 arc_size=0.4, 1044 s=1, 1045 italic=0, 1046 rotation=0, 1047 prefix="", 1048 lc="k2", 1049 c="white", 1050 alpha=1, 1051 lw=2, 1052 precision=3, 1053): 1054 """ 1055 Build a graphical goniometer to measure the angle formed by 3 points in space. 1056 1057 Arguments: 1058 p1 : (list) 1059 first point 3D coordinates. 1060 p2 : (list) 1061 the vertex point. 1062 p3 : (list) 1063 the last point defining the angle. 1064 font : (str) 1065 Font face. Check [available fonts here](https://vedo.embl.es/fonts). 1066 arc_size : (float) 1067 dimension of the arc wrt the smallest axis. 1068 s : (float) 1069 size of the text. 1070 italic : (float, bool) 1071 italic text. 1072 rotation : (float) 1073 rotation of text in degrees. 1074 prefix : (str) 1075 append this string to the numeric value of the angle. 1076 lc : (list) 1077 color of the goniometer lines. 1078 c : (str) 1079 color of the goniometer angle filling. Set alpha=0 to remove it. 1080 alpha : (float) 1081 transparency level. 1082 lw : (float) 1083 line width. 1084 precision : (int) 1085 number of significant digits. 1086 1087 Examples: 1088 - [goniometer.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/goniometer.py) 1089 1090 ![](https://vedo.embl.es/images/pyplot/goniometer.png) 1091 """ 1092 if isinstance(p1, Points): p1 = p1.pos() 1093 if isinstance(p2, Points): p2 = p2.pos() 1094 if isinstance(p3, Points): p3 = p3.pos() 1095 if len(p1)==2: p1=[p1[0], p1[1], 0.0] 1096 if len(p2)==2: p2=[p2[0], p2[1], 0.0] 1097 if len(p3)==2: p3=[p3[0], p3[1], 0.0] 1098 p1, p2, p3 = np.array(p1), np.array(p2), np.array(p3) 1099 1100 acts = [] 1101 ln = shapes.Line([p1, p2, p3], lw=lw, c=lc) 1102 acts.append(ln) 1103 1104 va = utils.versor(p1 - p2) 1105 vb = utils.versor(p3 - p2) 1106 r = min(utils.mag(p3 - p2), utils.mag(p1 - p2)) * arc_size 1107 ptsarc = [] 1108 res = 120 1109 imed = int(res / 2) 1110 for i in range(res + 1): 1111 vi = utils.versor(vb * i / res + va * (res - i) / res) 1112 if i == imed: 1113 vc = np.array(vi) 1114 ptsarc.append(p2 + vi * r) 1115 arc = shapes.Line(ptsarc).lw(lw).c(lc) 1116 acts.append(arc) 1117 1118 angle = np.arccos(np.dot(va, vb)) * 180 / np.pi 1119 1120 lb = shapes.Text3D( 1121 prefix + utils.precision(angle, precision) + "º", 1122 s=r / 12 * s, 1123 font=font, 1124 italic=italic, 1125 justify="center", 1126 ) 1127 cr = np.cross(va, vb) 1128 lb.reorient([0, 0, 1], cr * np.sign(cr[2]), rotation=rotation, xyplane=False) 1129 lb.pos(p2 + vc * r / 1.75) 1130 lb.c(c).bc("tomato").lighting("off") 1131 acts.append(lb) 1132 1133 if alpha > 0: 1134 pts = [p2] + arc.vertices.tolist() + [p2] 1135 msh = Mesh([pts, [list(range(arc.npoints + 2))]], c=lc, alpha=alpha) 1136 msh.lighting("off") 1137 msh.triangulate() 1138 msh.shift(0, 0, -r / 10000) # to resolve 2d conflicts.. 1139 acts.append(msh) 1140 1141 asse = Assembly(acts) 1142 asse.name = "Goniometer" 1143 return asse 1144 1145 1146def Light(pos, focal_point=(0, 0, 0), angle=180, c=None, intensity=1): 1147 """ 1148 Generate a source of light placed at `pos` and directed to `focal point`. 1149 Returns a `vtkLight` object. 1150 1151 Arguments: 1152 focal_point : (list) 1153 focal point, if a `vedo` object is passed then will grab its position. 1154 angle : (float) 1155 aperture angle of the light source, in degrees 1156 c : (color) 1157 set the light color 1158 intensity : (float) 1159 intensity value between 0 and 1. 1160 1161 Check also: 1162 `plotter.Plotter.remove_lights()` 1163 1164 Examples: 1165 - [light_sources.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/light_sources.py) 1166 1167 ![](https://vedo.embl.es/images/basic/lights.png) 1168 """ 1169 if c is None: 1170 try: 1171 c = pos.color() 1172 except AttributeError: 1173 c = "white" 1174 1175 try: 1176 pos = pos.pos() 1177 except AttributeError: 1178 pass 1179 1180 try: 1181 focal_point = focal_point.pos() 1182 except AttributeError: 1183 pass 1184 1185 light = vtki.vtkLight() 1186 light.SetLightTypeToSceneLight() 1187 light.SetPosition(pos) 1188 light.SetConeAngle(angle) 1189 light.SetFocalPoint(focal_point) 1190 light.SetIntensity(intensity) 1191 light.SetColor(get_color(c)) 1192 return light 1193 1194 1195##################################################################### 1196def ScalarBar( 1197 obj, 1198 title="", 1199 pos=(), 1200 size=(80, 400), 1201 font_size=14, 1202 title_yoffset=20, 1203 nlabels=None, 1204 c="k", 1205 horizontal=False, 1206 use_alpha=True, 1207 label_format=":6.3g", 1208) -> Union[vtki.vtkScalarBarActor, None]: 1209 """ 1210 A 2D scalar bar for the specified object. 1211 1212 Arguments: 1213 title : (str) 1214 scalar bar title 1215 pos : (list) 1216 position coordinates of the bottom left corner. 1217 Can also be a pair of (x,y) values in the range [0,1] 1218 to indicate the position of the bottom-left and top-right corners. 1219 size : (float,float) 1220 size of the scalarbar in number of pixels (width, height) 1221 font_size : (float) 1222 size of font for title and numeric labels 1223 title_yoffset : (float) 1224 vertical space offset between title and color scalarbar 1225 nlabels : (int) 1226 number of numeric labels 1227 c : (list) 1228 color of the scalar bar text 1229 horizontal : (bool) 1230 lay the scalarbar horizontally 1231 use_alpha : (bool) 1232 render transparency in the color bar itself 1233 label_format : (str) 1234 c-style format string for numeric labels 1235 1236 Examples: 1237 - [scalarbars.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/scalarbars.py) 1238 1239 ![](https://user-images.githubusercontent.com/32848391/62940174-4bdc7900-bdd3-11e9-9713-e4f3e2fdab63.png) 1240 """ 1241 1242 if isinstance(obj, (Points, TetMesh, vedo.UnstructuredGrid)): 1243 vtkscalars = obj.dataset.GetPointData().GetScalars() 1244 if vtkscalars is None: 1245 vtkscalars = obj.dataset.GetCellData().GetScalars() 1246 if not vtkscalars: 1247 return None 1248 lut = vtkscalars.GetLookupTable() 1249 if not lut: 1250 lut = obj.mapper.GetLookupTable() 1251 if not lut: 1252 return None 1253 1254 elif isinstance(obj, Volume): 1255 lut = utils.ctf2lut(obj) 1256 1257 elif utils.is_sequence(obj) and len(obj) == 2: 1258 x = np.linspace(obj[0], obj[1], 256) 1259 data = [] 1260 for i in range(256): 1261 rgb = color_map(i, c, 0, 256) 1262 data.append([x[i], rgb]) 1263 lut = build_lut(data) 1264 1265 elif not hasattr(obj, "mapper"): 1266 vedo.logger.error(f"in add_scalarbar(): input is invalid {type(obj)}. Skip.") 1267 return None 1268 1269 else: 1270 return None 1271 1272 c = get_color(c) 1273 sb = vtki.vtkScalarBarActor() 1274 1275 # print("GetLabelFormat", sb.GetLabelFormat()) 1276 label_format = label_format.replace(":", "%-#") 1277 sb.SetLabelFormat(label_format) 1278 1279 sb.SetLookupTable(lut) 1280 sb.SetUseOpacity(use_alpha) 1281 sb.SetDrawFrame(0) 1282 sb.SetDrawBackground(0) 1283 if lut.GetUseBelowRangeColor(): 1284 sb.DrawBelowRangeSwatchOn() 1285 sb.SetBelowRangeAnnotation("") 1286 if lut.GetUseAboveRangeColor(): 1287 sb.DrawAboveRangeSwatchOn() 1288 sb.SetAboveRangeAnnotation("") 1289 if lut.GetNanColor() != (0.5, 0.0, 0.0, 1.0): 1290 sb.DrawNanAnnotationOn() 1291 sb.SetNanAnnotation("nan") 1292 1293 if title: 1294 if "\\" in repr(title): 1295 for r in shapes._reps: 1296 title = title.replace(r[0], r[1]) 1297 titprop = sb.GetTitleTextProperty() 1298 titprop.BoldOn() 1299 titprop.ItalicOff() 1300 titprop.ShadowOff() 1301 titprop.SetColor(c) 1302 titprop.SetVerticalJustificationToTop() 1303 titprop.SetFontSize(font_size) 1304 titprop.SetFontFamily(vtki.VTK_FONT_FILE) 1305 titprop.SetFontFile(utils.get_font_path(vedo.settings.default_font)) 1306 sb.SetTitle(title) 1307 sb.SetVerticalTitleSeparation(title_yoffset) 1308 sb.SetTitleTextProperty(titprop) 1309 1310 sb.SetTextPad(0) 1311 sb.UnconstrainedFontSizeOn() 1312 sb.DrawAnnotationsOn() 1313 sb.DrawTickLabelsOn() 1314 sb.SetMaximumNumberOfColors(256) 1315 if nlabels is not None: 1316 sb.SetNumberOfLabels(nlabels) 1317 1318 if len(pos) == 0 or utils.is_sequence(pos[0]): 1319 if len(pos) == 0: 1320 pos = ((0.87, 0.05), (0.97, 0.5)) 1321 if horizontal: 1322 pos = ((0.5, 0.05), (0.97, 0.15)) 1323 sb.SetTextPositionToPrecedeScalarBar() 1324 if horizontal: 1325 if not nlabels: sb.SetNumberOfLabels(3) 1326 sb.SetOrientationToHorizontal() 1327 sb.SetTextPositionToSucceedScalarBar() 1328 sb.GetPositionCoordinate().SetCoordinateSystemToNormalizedViewport() 1329 sb.GetPosition2Coordinate().SetCoordinateSystemToNormalizedViewport() 1330 1331 s = np.array(pos[1]) - np.array(pos[0]) 1332 sb.GetPositionCoordinate().SetValue(pos[0][0], pos[0][1]) 1333 sb.GetPosition2Coordinate().SetValue(s[0], s[1]) # size !!?? 1334 1335 else: 1336 1337 if horizontal: 1338 size = (size[1], size[0]) # swap size 1339 sb.SetPosition(pos[0]-0.7, pos[1]) 1340 if not nlabels: sb.SetNumberOfLabels(3) 1341 sb.SetOrientationToHorizontal() 1342 sb.SetTextPositionToSucceedScalarBar() 1343 else: 1344 sb.SetPosition(pos[0], pos[1]) 1345 if not nlabels: sb.SetNumberOfLabels(7) 1346 sb.SetTextPositionToPrecedeScalarBar() 1347 sb.SetHeight(1) 1348 sb.SetWidth(1) 1349 if size[0] is not None: sb.SetMaximumWidthInPixels(size[0]) 1350 if size[1] is not None: sb.SetMaximumHeightInPixels(size[1]) 1351 1352 sctxt = sb.GetLabelTextProperty() 1353 sctxt.SetFontFamily(vtki.VTK_FONT_FILE) 1354 sctxt.SetFontFile(utils.get_font_path(vedo.settings.default_font)) 1355 sctxt.SetColor(c) 1356 sctxt.SetShadow(0) 1357 sctxt.SetFontSize(font_size) 1358 sb.SetAnnotationTextProperty(sctxt) 1359 sb.PickableOff() 1360 return sb 1361 1362 1363##################################################################### 1364def ScalarBar3D( 1365 obj, 1366 title="", 1367 pos=None, 1368 size=(0, 0), 1369 title_font="", 1370 title_xoffset=-1.2, 1371 title_yoffset=0.0, 1372 title_size=1.5, 1373 title_rotation=0.0, 1374 nlabels=8, 1375 label_font="", 1376 label_size=1, 1377 label_offset=0.375, 1378 label_rotation=0, 1379 label_format="", 1380 italic=0, 1381 c="k", 1382 draw_box=True, 1383 above_text=None, 1384 below_text=None, 1385 nan_text="NaN", 1386 categories=None, 1387) -> Union[Assembly, None]: 1388 """ 1389 Create a 3D scalar bar for the specified object. 1390 1391 Input `obj` input can be: 1392 1393 - a list of numbers, 1394 - a list of two numbers in the form (min, max), 1395 - a Mesh already containing a set of scalars associated to vertices or cells, 1396 - if None the last object in the list of actors will be used. 1397 1398 Arguments: 1399 size : (list) 1400 (thickness, length) of scalarbar 1401 title : (str) 1402 scalar bar title 1403 title_xoffset : (float) 1404 horizontal space btw title and color scalarbar 1405 title_yoffset : (float) 1406 vertical space offset 1407 title_size : (float) 1408 size of title wrt numeric labels 1409 title_rotation : (float) 1410 title rotation in degrees 1411 nlabels : (int) 1412 number of numeric labels 1413 label_font : (str) 1414 font type for labels 1415 label_size : (float) 1416 label scale factor 1417 label_offset : (float) 1418 space btw numeric labels and scale 1419 label_rotation : (float) 1420 label rotation in degrees 1421 draw_box : (bool) 1422 draw a box around the colorbar 1423 categories : (list) 1424 make a categorical scalarbar, 1425 the input list will have the format [value, color, alpha, textlabel] 1426 1427 Examples: 1428 - [scalarbars.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/scalarbars.py) 1429 - [plot_fxy2.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/plot_fxy2.py) 1430 """ 1431 1432 if isinstance(obj, (Points, TetMesh, vedo.UnstructuredGrid)): 1433 lut = obj.mapper.GetLookupTable() 1434 if not lut or lut.GetTable().GetNumberOfTuples() == 0: 1435 # create the most similar to the default 1436 obj.cmap("jet_r") 1437 lut = obj.mapper.GetLookupTable() 1438 vmin, vmax = lut.GetRange() 1439 1440 elif isinstance(obj, Volume): 1441 lut = utils.ctf2lut(obj) 1442 vmin, vmax = lut.GetRange() 1443 1444 else: 1445 vedo.logger.error("in ScalarBar3D(): input must be a vedo object with bounds.") 1446 return None 1447 1448 bns = obj.bounds() 1449 sx, sy = size 1450 if sy == 0 or sy is None: 1451 sy = bns[3] - bns[2] 1452 if sx == 0 or sx is None: 1453 sx = sy / 18 1454 1455 if categories is not None: ################################ 1456 ncats = len(categories) 1457 scale = shapes.Grid([-float(sx) * label_offset, 0, 0], 1458 c=c, alpha=1, s=(sx, sy), res=(1, ncats)) 1459 cols, alphas = [], [] 1460 ticks_pos, ticks_txt = [0.0], [""] 1461 for i, cat in enumerate(categories): 1462 cl = get_color(cat[1]) 1463 cols.append(cl) 1464 if len(cat) > 2: 1465 alphas.append(cat[2]) 1466 else: 1467 alphas.append(1) 1468 if len(cat) > 3: 1469 ticks_txt.append(cat[3]) 1470 else: 1471 ticks_txt.append("") 1472 ticks_pos.append((i + 0.5) / ncats) 1473 ticks_pos.append(1.0) 1474 ticks_txt.append("") 1475 rgba = np.c_[np.array(cols) * 255, np.array(alphas) * 255] 1476 scale.cellcolors = rgba 1477 1478 else: ######################################################## 1479 1480 # build the color scale part 1481 scale = shapes.Grid( 1482 [-float(sx) * label_offset, 0, 0], 1483 c=c, 1484 s=(sx, sy), 1485 res=(1, lut.GetTable().GetNumberOfTuples()), 1486 ) 1487 cscals = np.linspace(vmin, vmax, lut.GetTable().GetNumberOfTuples(), endpoint=True) 1488 1489 if lut.GetScale(): # logarithmic scale 1490 lut10 = vtki.vtkLookupTable() 1491 lut10.DeepCopy(lut) 1492 lut10.SetScaleToLinear() 1493 lut10.Build() 1494 scale.cmap(lut10, cscals, on="cells") 1495 tk = utils.make_ticks(vmin, vmax, nlabels, logscale=True, useformat=label_format) 1496 else: 1497 # for i in range(lut.GetTable().GetNumberOfTuples()): 1498 # print("LUT i=", i, lut.GetTableValue(i)) 1499 scale.cmap(lut, cscals, on="cells") 1500 tk = utils.make_ticks(vmin, vmax, nlabels, logscale=False, useformat=label_format) 1501 ticks_pos, ticks_txt = tk 1502 1503 scale.lw(0).wireframe(False).lighting("off") 1504 1505 scales = [scale] 1506 1507 xbns = scale.xbounds() 1508 1509 lsize = sy / 60 * label_size 1510 1511 tacts = [] 1512 for i, p in enumerate(ticks_pos): 1513 tx = ticks_txt[i] 1514 if i and tx: 1515 # build numeric text 1516 y = (p - 0.5) * sy 1517 if label_rotation: 1518 a = shapes.Text3D( 1519 tx, 1520 s=lsize, 1521 justify="center-top", 1522 c=c, 1523 italic=italic, 1524 font=label_font, 1525 ) 1526 a.rotate_z(label_rotation) 1527 a.pos(sx * label_offset, y, 0) 1528 else: 1529 a = shapes.Text3D( 1530 tx, 1531 pos=[sx * label_offset, y, 0], 1532 s=lsize, 1533 justify="center-left", 1534 c=c, 1535 italic=italic, 1536 font=label_font, 1537 ) 1538 1539 tacts.append(a) 1540 1541 # build ticks 1542 tic = shapes.Line([xbns[1], y, 0], [xbns[1] + sx * label_offset / 4, y, 0], lw=2, c=c) 1543 tacts.append(tic) 1544 1545 # build title 1546 if title: 1547 t = shapes.Text3D( 1548 title, 1549 pos=(0, 0, 0), 1550 s=sy / 50 * title_size, 1551 c=c, 1552 justify="centered-bottom", 1553 italic=italic, 1554 font=title_font, 1555 ) 1556 t.rotate_z(90 + title_rotation) 1557 t.pos(sx * title_xoffset, title_yoffset, 0) 1558 tacts.append(t) 1559 1560 if pos is None: 1561 tsize = 0 1562 if title: 1563 bbt = t.bounds() 1564 tsize = bbt[1] - bbt[0] 1565 pos = (bns[1] + tsize + sx * 1.5, (bns[2] + bns[3]) / 2, bns[4]) 1566 1567 # build below scale 1568 if lut.GetUseBelowRangeColor(): 1569 r, g, b, alfa = lut.GetBelowRangeColor() 1570 sx = float(sx) 1571 sy = float(sy) 1572 brect = shapes.Rectangle( 1573 [-sx * label_offset - sx / 2, -sy / 2 - sx - sx * 0.1, 0], 1574 [-sx * label_offset + sx / 2, -sy / 2 - sx * 0.1, 0], 1575 c=(r, g, b), 1576 alpha=alfa, 1577 ) 1578 brect.lw(1).lc(c).lighting("off") 1579 scales += [brect] 1580 if below_text is None: 1581 below_text = " <" + str(vmin) 1582 if below_text: 1583 if label_rotation: 1584 btx = shapes.Text3D( 1585 below_text, 1586 pos=(0, 0, 0), 1587 s=lsize, 1588 c=c, 1589 justify="center-top", 1590 italic=italic, 1591 font=label_font, 1592 ) 1593 btx.rotate_z(label_rotation) 1594 else: 1595 btx = shapes.Text3D( 1596 below_text, 1597 pos=(0, 0, 0), 1598 s=lsize, 1599 c=c, 1600 justify="center-left", 1601 italic=italic, 1602 font=label_font, 1603 ) 1604 1605 btx.pos(sx * label_offset, -sy / 2 - sx * 0.66, 0) 1606 tacts.append(btx) 1607 1608 # build above scale 1609 if lut.GetUseAboveRangeColor(): 1610 r, g, b, alfa = lut.GetAboveRangeColor() 1611 arect = shapes.Rectangle( 1612 [-sx * label_offset - sx / 2, sy / 2 + sx * 0.1, 0], 1613 [-sx * label_offset + sx / 2, sy / 2 + sx + sx * 0.1, 0], 1614 c=(r, g, b), 1615 alpha=alfa, 1616 ) 1617 arect.lw(1).lc(c).lighting("off") 1618 scales += [arect] 1619 if above_text is None: 1620 above_text = " >" + str(vmax) 1621 if above_text: 1622 if label_rotation: 1623 atx = shapes.Text3D( 1624 above_text, 1625 pos=(0, 0, 0), 1626 s=lsize, 1627 c=c, 1628 justify="center-top", 1629 italic=italic, 1630 font=label_font, 1631 ) 1632 atx.rotate_z(label_rotation) 1633 else: 1634 atx = shapes.Text3D( 1635 above_text, 1636 pos=(0, 0, 0), 1637 s=lsize, 1638 c=c, 1639 justify="center-left", 1640 italic=italic, 1641 font=label_font, 1642 ) 1643 1644 atx.pos(sx * label_offset, sy / 2 + sx * 0.66, 0) 1645 tacts.append(atx) 1646 1647 # build NaN scale 1648 if lut.GetNanColor() != (0.5, 0.0, 0.0, 1.0): 1649 nanshift = sx * 0.1 1650 if brect: 1651 nanshift += sx 1652 r, g, b, alfa = lut.GetNanColor() 1653 nanrect = shapes.Rectangle( 1654 [-sx * label_offset - sx / 2, -sy / 2 - sx - sx * 0.1 - nanshift, 0], 1655 [-sx * label_offset + sx / 2, -sy / 2 - sx * 0.1 - nanshift, 0], 1656 c=(r, g, b), 1657 alpha=alfa, 1658 ) 1659 nanrect.lw(1).lc(c).lighting("off") 1660 scales += [nanrect] 1661 if label_rotation: 1662 nantx = shapes.Text3D( 1663 nan_text, 1664 pos=(0, 0, 0), 1665 s=lsize, 1666 c=c, 1667 justify="center-left", 1668 italic=italic, 1669 font=label_font, 1670 ) 1671 nantx.rotate_z(label_rotation) 1672 else: 1673 nantx = shapes.Text3D( 1674 nan_text, 1675 pos=(0, 0, 0), 1676 s=lsize, 1677 c=c, 1678 justify="center-left", 1679 italic=italic, 1680 font=label_font, 1681 ) 1682 nantx.pos(sx * label_offset, -sy / 2 - sx * 0.66 - nanshift, 0) 1683 tacts.append(nantx) 1684 1685 if draw_box: 1686 tacts.append(scale.box().lw(1).c(c)) 1687 1688 for m in tacts + scales: 1689 m.shift(pos) 1690 m.actor.PickableOff() 1691 m.properties.LightingOff() 1692 1693 asse = Assembly(scales + tacts) 1694 1695 # asse.transform = LinearTransform().shift(pos) 1696 1697 bb = asse.GetBounds() 1698 # print("ScalarBar3D pos",pos, bb) 1699 # asse.SetOrigin(pos) 1700 1701 asse.SetOrigin(bb[0], bb[2], bb[4]) 1702 # asse.SetOrigin(bb[0],0,0) #in pyplot line 1312 1703 1704 asse.PickableOff() 1705 asse.UseBoundsOff() 1706 asse.name = "ScalarBar3D" 1707 return asse 1708 1709 1710##################################################################### 1711class Slider2D(SliderWidget): 1712 """ 1713 Add a slider which can call an external custom function. 1714 """ 1715 1716 def __init__( 1717 self, 1718 sliderfunc, 1719 xmin, 1720 xmax, 1721 value=None, 1722 pos=4, 1723 title="", 1724 font="Calco", 1725 title_size=1, 1726 c="k", 1727 alpha=1, 1728 show_value=True, 1729 delayed=False, 1730 **options, 1731 ): 1732 """ 1733 Add a slider which can call an external custom function. 1734 Set any value as float to increase the number of significant digits above the slider. 1735 1736 Use `play()` to start an animation between the current slider value and the last value. 1737 1738 Arguments: 1739 sliderfunc : (function) 1740 external function to be called by the widget 1741 xmin : (float) 1742 lower value of the slider 1743 xmax : (float) 1744 upper value 1745 value : (float) 1746 current value 1747 pos : (list, str) 1748 position corner number: horizontal [1-5] or vertical [11-15] 1749 it can also be specified by corners coordinates [(x1,y1), (x2,y2)] 1750 and also by a string descriptor (eg. "bottom-left") 1751 title : (str) 1752 title text 1753 font : (str) 1754 title font face. Check [available fonts here](https://vedo.embl.es/fonts). 1755 title_size : (float) 1756 title text scale [1.0] 1757 show_value : (bool) 1758 if True current value is shown 1759 delayed : (bool) 1760 if True the callback is delayed until when the mouse button is released 1761 alpha : (float) 1762 opacity of the scalar bar texts 1763 slider_length : (float) 1764 slider length 1765 slider_width : (float) 1766 slider width 1767 end_cap_length : (float) 1768 length of the end cap 1769 end_cap_width : (float) 1770 width of the end cap 1771 tube_width : (float) 1772 width of the tube 1773 title_height : (float) 1774 height of the title 1775 tformat : (str) 1776 format of the title 1777 1778 Examples: 1779 - [sliders1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/sliders1.py) 1780 - [sliders2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/sliders2.py) 1781 1782 ![](https://user-images.githubusercontent.com/32848391/50738848-be033480-11d8-11e9-9b1a-c13105423a79.jpg) 1783 """ 1784 slider_length = options.pop("slider_length", 0.015) 1785 slider_width = options.pop("slider_width", 0.025) 1786 end_cap_length= options.pop("end_cap_length", 0.0015) 1787 end_cap_width = options.pop("end_cap_width", 0.0125) 1788 tube_width = options.pop("tube_width", 0.0075) 1789 title_height = options.pop("title_height", 0.025) 1790 tformat = options.pop("tformat", None) 1791 1792 if options: 1793 vedo.logger.warning(f"in Slider2D unknown option(s): {options}") 1794 1795 c = get_color(c) 1796 1797 if value is None or value < xmin: 1798 value = xmin 1799 1800 slider_rep = vtki.new("SliderRepresentation2D") 1801 slider_rep.SetMinimumValue(xmin) 1802 slider_rep.SetMaximumValue(xmax) 1803 slider_rep.SetValue(value) 1804 slider_rep.SetSliderLength(slider_length) 1805 slider_rep.SetSliderWidth(slider_width) 1806 slider_rep.SetEndCapLength(end_cap_length) 1807 slider_rep.SetEndCapWidth(end_cap_width) 1808 slider_rep.SetTubeWidth(tube_width) 1809 slider_rep.GetPoint1Coordinate().SetCoordinateSystemToNormalizedDisplay() 1810 slider_rep.GetPoint2Coordinate().SetCoordinateSystemToNormalizedDisplay() 1811 1812 if isinstance(pos, str): 1813 if "top" in pos: 1814 if "left" in pos: 1815 if "vert" in pos: 1816 pos = 11 1817 else: 1818 pos = 1 1819 elif "right" in pos: 1820 if "vert" in pos: 1821 pos = 12 1822 else: 1823 pos = 2 1824 elif "bott" in pos: 1825 if "left" in pos: 1826 if "vert" in pos: 1827 pos = 13 1828 else: 1829 pos = 3 1830 elif "right" in pos: 1831 if "vert" in pos: 1832 if "span" in pos: 1833 pos = 15 1834 else: 1835 pos = 14 1836 else: 1837 pos = 4 1838 elif "span" in pos: 1839 pos = 5 1840 1841 if utils.is_sequence(pos): 1842 slider_rep.GetPoint1Coordinate().SetValue(pos[0][0], pos[0][1]) 1843 slider_rep.GetPoint2Coordinate().SetValue(pos[1][0], pos[1][1]) 1844 elif pos == 1: # top-left horizontal 1845 slider_rep.GetPoint1Coordinate().SetValue(0.04, 0.93) 1846 slider_rep.GetPoint2Coordinate().SetValue(0.45, 0.93) 1847 elif pos == 2: 1848 slider_rep.GetPoint1Coordinate().SetValue(0.55, 0.93) 1849 slider_rep.GetPoint2Coordinate().SetValue(0.95, 0.93) 1850 elif pos == 3: 1851 slider_rep.GetPoint1Coordinate().SetValue(0.05, 0.06) 1852 slider_rep.GetPoint2Coordinate().SetValue(0.45, 0.06) 1853 elif pos == 4: # bottom-right 1854 slider_rep.GetPoint1Coordinate().SetValue(0.55, 0.06) 1855 slider_rep.GetPoint2Coordinate().SetValue(0.95, 0.06) 1856 elif pos == 5: # bottom span horizontal 1857 slider_rep.GetPoint1Coordinate().SetValue(0.04, 0.06) 1858 slider_rep.GetPoint2Coordinate().SetValue(0.95, 0.06) 1859 elif pos == 11: # top-left vertical 1860 slider_rep.GetPoint1Coordinate().SetValue(0.065, 0.54) 1861 slider_rep.GetPoint2Coordinate().SetValue(0.065, 0.9) 1862 elif pos == 12: 1863 slider_rep.GetPoint1Coordinate().SetValue(0.94, 0.54) 1864 slider_rep.GetPoint2Coordinate().SetValue(0.94, 0.9) 1865 elif pos == 13: 1866 slider_rep.GetPoint1Coordinate().SetValue(0.065, 0.1) 1867 slider_rep.GetPoint2Coordinate().SetValue(0.065, 0.54) 1868 elif pos == 14: # bottom-right vertical 1869 slider_rep.GetPoint1Coordinate().SetValue(0.94, 0.1) 1870 slider_rep.GetPoint2Coordinate().SetValue(0.94, 0.54) 1871 elif pos == 15: # right margin vertical 1872 slider_rep.GetPoint1Coordinate().SetValue(0.95, 0.1) 1873 slider_rep.GetPoint2Coordinate().SetValue(0.95, 0.9) 1874 else: # bottom-right 1875 slider_rep.GetPoint1Coordinate().SetValue(0.55, 0.06) 1876 slider_rep.GetPoint2Coordinate().SetValue(0.95, 0.06) 1877 1878 if show_value: 1879 if tformat is None: 1880 if isinstance(xmin, int) and isinstance(xmax, int) and isinstance(value, int): 1881 tformat = "%0.0f" 1882 else: 1883 tformat = "%0.2f" 1884 1885 slider_rep.SetLabelFormat(tformat) # default is '%0.3g' 1886 slider_rep.GetLabelProperty().SetShadow(0) 1887 slider_rep.GetLabelProperty().SetBold(0) 1888 slider_rep.GetLabelProperty().SetOpacity(alpha) 1889 slider_rep.GetLabelProperty().SetColor(c) 1890 if isinstance(pos, int) and pos > 10: 1891 slider_rep.GetLabelProperty().SetOrientation(90) 1892 else: 1893 slider_rep.ShowSliderLabelOff() 1894 slider_rep.GetTubeProperty().SetColor(c) 1895 slider_rep.GetTubeProperty().SetOpacity(0.75) 1896 slider_rep.GetSliderProperty().SetColor(c) 1897 slider_rep.GetSelectedProperty().SetColor(np.sqrt(np.array(c))) 1898 slider_rep.GetCapProperty().SetColor(c) 1899 1900 slider_rep.SetTitleHeight(title_height * title_size) 1901 slider_rep.GetTitleProperty().SetShadow(0) 1902 slider_rep.GetTitleProperty().SetColor(c) 1903 slider_rep.GetTitleProperty().SetOpacity(alpha) 1904 slider_rep.GetTitleProperty().SetBold(0) 1905 if font.lower() == "courier": 1906 slider_rep.GetTitleProperty().SetFontFamilyToCourier() 1907 elif font.lower() == "times": 1908 slider_rep.GetTitleProperty().SetFontFamilyToTimes() 1909 elif font.lower() == "arial": 1910 slider_rep.GetTitleProperty().SetFontFamilyToArial() 1911 else: 1912 if font == "": 1913 font = utils.get_font_path(settings.default_font) 1914 else: 1915 font = utils.get_font_path(font) 1916 slider_rep.GetTitleProperty().SetFontFamily(vtki.VTK_FONT_FILE) 1917 slider_rep.GetLabelProperty().SetFontFamily(vtki.VTK_FONT_FILE) 1918 slider_rep.GetTitleProperty().SetFontFile(font) 1919 slider_rep.GetLabelProperty().SetFontFile(font) 1920 1921 if title: 1922 slider_rep.SetTitleText(title) 1923 if not utils.is_sequence(pos): 1924 if isinstance(pos, int) and pos > 10: 1925 slider_rep.GetTitleProperty().SetOrientation(90) 1926 else: 1927 if abs(pos[0][0] - pos[1][0]) < 0.1: 1928 slider_rep.GetTitleProperty().SetOrientation(90) 1929 1930 super().__init__() 1931 1932 self.SetAnimationModeToJump() 1933 self.SetRepresentation(slider_rep) 1934 if delayed: 1935 self.AddObserver("EndInteractionEvent", sliderfunc) 1936 else: 1937 self.AddObserver("InteractionEvent", sliderfunc) 1938 1939 1940##################################################################### 1941class Slider3D(SliderWidget): 1942 """ 1943 Add a 3D slider which can call an external custom function. 1944 """ 1945 1946 def __init__( 1947 self, 1948 sliderfunc, 1949 pos1, 1950 pos2, 1951 xmin, 1952 xmax, 1953 value=None, 1954 s=0.03, 1955 t=1, 1956 title="", 1957 rotation=0, 1958 c=None, 1959 show_value=True, 1960 ): 1961 """ 1962 Add a 3D slider which can call an external custom function. 1963 1964 Arguments: 1965 sliderfunc : (function) 1966 external function to be called by the widget 1967 pos1 : (list) 1968 first position 3D coordinates 1969 pos2 : (list) 1970 second position 3D coordinates 1971 xmin : (float) 1972 lower value 1973 xmax : (float) 1974 upper value 1975 value : (float) 1976 initial value 1977 s : (float) 1978 label scaling factor 1979 t : (float) 1980 tube scaling factor 1981 title : (str) 1982 title text 1983 c : (color) 1984 slider color 1985 rotation : (float) 1986 title rotation around slider axis 1987 show_value : (bool) 1988 if True current value is shown on top of the slider 1989 1990 Examples: 1991 - [sliders3d.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/sliders3d.py) 1992 """ 1993 c = get_color(c) 1994 1995 if value is None or value < xmin: 1996 value = xmin 1997 1998 slider_rep = vtki.new("SliderRepresentation3D") 1999 slider_rep.SetMinimumValue(xmin) 2000 slider_rep.SetMaximumValue(xmax) 2001 slider_rep.SetValue(value) 2002 2003 slider_rep.GetPoint1Coordinate().SetCoordinateSystemToWorld() 2004 slider_rep.GetPoint2Coordinate().SetCoordinateSystemToWorld() 2005 slider_rep.GetPoint1Coordinate().SetValue(pos2) 2006 slider_rep.GetPoint2Coordinate().SetValue(pos1) 2007 2008 # slider_rep.SetPoint1InWorldCoordinates(pos2[0], pos2[1], pos2[2]) 2009 # slider_rep.SetPoint2InWorldCoordinates(pos1[0], pos1[1], pos1[2]) 2010 2011 slider_rep.SetSliderWidth(0.03 * t) 2012 slider_rep.SetTubeWidth(0.01 * t) 2013 slider_rep.SetSliderLength(0.04 * t) 2014 slider_rep.SetSliderShapeToCylinder() 2015 slider_rep.GetSelectedProperty().SetColor(np.sqrt(np.array(c))) 2016 slider_rep.GetSliderProperty().SetColor(np.array(c) / 1.5) 2017 slider_rep.GetCapProperty().SetOpacity(0) 2018 slider_rep.SetRotation(rotation) 2019 2020 if not show_value: 2021 slider_rep.ShowSliderLabelOff() 2022 2023 slider_rep.SetTitleText(title) 2024 slider_rep.SetTitleHeight(s * t) 2025 slider_rep.SetLabelHeight(s * t * 0.85) 2026 2027 slider_rep.GetTubeProperty().SetColor(c) 2028 2029 super().__init__() 2030 2031 self.SetRepresentation(slider_rep) 2032 self.SetAnimationModeToJump() 2033 self.AddObserver("InteractionEvent", sliderfunc) 2034 2035 2036class BaseCutter: 2037 """ 2038 Base class for Cutter widgets. 2039 """ 2040 2041 def __init__(self): 2042 self._implicit_func = None 2043 self.widget = None 2044 self.clipper = None 2045 self.cutter = None 2046 self.mesh = None 2047 self.remnant = None 2048 self._alpha = 0.5 2049 self._keypress_id = None 2050 2051 def invert(self) -> Self: 2052 """Invert selection.""" 2053 self.clipper.SetInsideOut(not self.clipper.GetInsideOut()) 2054 return self 2055 2056 def bounds(self, value=None) -> Union[Self, np.ndarray]: 2057 """Set or get the bounding box.""" 2058 if value is None: 2059 return self.cutter.GetBounds() 2060 else: 2061 self._implicit_func.SetBounds(value) 2062 return self 2063 2064 def on(self) -> Self: 2065 """Switch the widget on or off.""" 2066 self.widget.On() 2067 return self 2068 2069 def off(self) -> Self: 2070 """Switch the widget on or off.""" 2071 self.widget.Off() 2072 return self 2073 2074 def add_to(self, plt) -> Self: 2075 """Assign the widget to the provided `Plotter` instance.""" 2076 self.widget.SetInteractor(plt.interactor) 2077 self.widget.SetCurrentRenderer(plt.renderer) 2078 if self.widget not in plt.widgets: 2079 plt.widgets.append(self.widget) 2080 2081 cpoly = self.clipper.GetOutput() 2082 self.mesh._update(cpoly) 2083 2084 out = self.clipper.GetClippedOutputPort() 2085 if self._alpha: 2086 self.remnant.mapper.SetInputConnection(out) 2087 self.remnant.alpha(self._alpha).color((0.5, 0.5, 0.5)) 2088 self.remnant.lighting("off").wireframe() 2089 plt.add(self.mesh, self.remnant) 2090 else: 2091 plt.add(self.mesh) 2092 2093 self._keypress_id = plt.interactor.AddObserver( 2094 "KeyPressEvent", self._keypress 2095 ) 2096 if plt.interactor and plt.interactor.GetInitialized(): 2097 self.widget.On() 2098 self._select_polygons(self.widget, "InteractionEvent") 2099 plt.interactor.Render() 2100 return self 2101 2102 def remove_from(self, plt) -> Self: 2103 """Remove the widget to the provided `Plotter` instance.""" 2104 self.widget.Off() 2105 self.widget.RemoveAllObservers() ### NOT SURE 2106 plt.remove(self.remnant) 2107 if self.widget in plt.widgets: 2108 plt.widgets.remove(self.widget) 2109 if self._keypress_id: 2110 plt.interactor.RemoveObserver(self._keypress_id) 2111 return self 2112 2113 def add_observer(self, event, func, priority=1) -> int: 2114 """Add an observer to the widget.""" 2115 event = utils.get_vtk_name_event(event) 2116 cid = self.widget.AddObserver(event, func, priority) 2117 return cid 2118 2119 2120class PlaneCutter(vtki.vtkPlaneWidget, BaseCutter): 2121 """ 2122 Create a box widget to cut away parts of a Mesh. 2123 """ 2124 2125 def __init__( 2126 self, 2127 mesh, 2128 invert=False, 2129 can_translate=True, 2130 can_scale=True, 2131 origin=(), 2132 normal=(), 2133 padding=0.05, 2134 delayed=False, 2135 c=(0.25, 0.25, 0.25), 2136 alpha=0.05, 2137 ): 2138 """ 2139 Create a box widget to cut away parts of a `Mesh`. 2140 2141 Arguments: 2142 mesh : (Mesh) 2143 the input mesh 2144 invert : (bool) 2145 invert the clipping plane 2146 can_translate : (bool) 2147 enable translation of the widget 2148 can_scale : (bool) 2149 enable scaling of the widget 2150 origin : (list) 2151 origin of the plane 2152 normal : (list) 2153 normal to the plane 2154 padding : (float) 2155 padding around the input mesh 2156 delayed : (bool) 2157 if True the callback is delayed until 2158 when the mouse button is released (useful for large meshes) 2159 c : (color) 2160 color of the box cutter widget 2161 alpha : (float) 2162 transparency of the cut-off part of the input mesh 2163 2164 Examples: 2165 - [slice_plane3.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/slice_plane3.py) 2166 """ 2167 super().__init__() 2168 2169 self.mesh = mesh 2170 self.remnant = Mesh() 2171 self.remnant.name = mesh.name + "Remnant" 2172 self.remnant.pickable(False) 2173 2174 self._alpha = alpha 2175 self._keypress_id = None 2176 2177 self._implicit_func = vtki.new("Plane") 2178 2179 poly = mesh.dataset 2180 self.clipper = vtki.new("ClipPolyData") 2181 self.clipper.GenerateClipScalarsOff() 2182 self.clipper.SetInputData(poly) 2183 self.clipper.SetClipFunction(self._implicit_func) 2184 self.clipper.SetInsideOut(invert) 2185 self.clipper.GenerateClippedOutputOn() 2186 self.clipper.Update() 2187 2188 self.widget = vtki.new("ImplicitPlaneWidget") 2189 2190 # self.widget.KeyPressActivationOff() 2191 # self.widget.SetKeyPressActivationValue('i') 2192 2193 self.widget.SetOriginTranslation(can_translate) 2194 self.widget.SetOutlineTranslation(can_translate) 2195 self.widget.SetScaleEnabled(can_scale) 2196 2197 self.widget.GetOutlineProperty().SetColor(get_color(c)) 2198 self.widget.GetOutlineProperty().SetOpacity(0.25) 2199 self.widget.GetOutlineProperty().SetLineWidth(1) 2200 self.widget.GetOutlineProperty().LightingOff() 2201 2202 self.widget.GetSelectedOutlineProperty().SetColor(get_color("red3")) 2203 2204 self.widget.SetTubing(0) 2205 self.widget.SetDrawPlane(bool(alpha)) 2206 self.widget.GetPlaneProperty().LightingOff() 2207 self.widget.GetPlaneProperty().SetOpacity(alpha) 2208 self.widget.GetSelectedPlaneProperty().SetColor(get_color("red5")) 2209 self.widget.GetSelectedPlaneProperty().LightingOff() 2210 2211 self.widget.SetPlaceFactor(1.0 + padding) 2212 self.widget.SetInputData(poly) 2213 self.widget.PlaceWidget() 2214 if delayed: 2215 self.widget.AddObserver("EndInteractionEvent", self._select_polygons) 2216 else: 2217 self.widget.AddObserver("InteractionEvent", self._select_polygons) 2218 2219 if len(origin) == 3: 2220 self.widget.SetOrigin(origin) 2221 else: 2222 self.widget.SetOrigin(mesh.center_of_mass()) 2223 2224 if len(normal) == 3: 2225 self.widget.SetNormal(normal) 2226 else: 2227 self.widget.SetNormal((1, 0, 0)) 2228 2229 @property 2230 def origin(self): 2231 """Get the origin of the plane.""" 2232 return np.array(self.widget.GetOrigin()) 2233 2234 @origin.setter 2235 def origin(self, value): 2236 """Set the origin of the plane.""" 2237 self.widget.SetOrigin(value) 2238 2239 @property 2240 def normal(self): 2241 """Get the normal of the plane.""" 2242 return np.array(self.widget.GetNormal()) 2243 2244 @normal.setter 2245 def normal(self, value): 2246 """Set the normal of the plane.""" 2247 self.widget.SetNormal(value) 2248 2249 def _select_polygons(self, vobj, event) -> None: 2250 vobj.GetPlane(self._implicit_func) 2251 2252 def _keypress(self, vobj, event): 2253 if vobj.GetKeySym() == "r": # reset planes 2254 self.widget.GetPlane(self._implicit_func) 2255 self.widget.PlaceWidget() 2256 self.widget.GetInteractor().Render() 2257 elif vobj.GetKeySym() == "u": # invert cut 2258 self.invert() 2259 self.widget.GetInteractor().Render() 2260 elif vobj.GetKeySym() == "x": # set normal along x 2261 self.widget.SetNormal((1, 0, 0)) 2262 self.widget.GetPlane(self._implicit_func) 2263 self.widget.PlaceWidget() 2264 self.widget.GetInteractor().Render() 2265 elif vobj.GetKeySym() == "y": # set normal along y 2266 self.widget.SetNormal((0, 1, 0)) 2267 self.widget.GetPlane(self._implicit_func) 2268 self.widget.PlaceWidget() 2269 self.widget.GetInteractor().Render() 2270 elif vobj.GetKeySym() == "z": # set normal along z 2271 self.widget.SetNormal((0, 0, 1)) 2272 self.widget.GetPlane(self._implicit_func) 2273 self.widget.PlaceWidget() 2274 self.widget.GetInteractor().Render() 2275 elif vobj.GetKeySym() == "s": # Ctrl+s to save mesh 2276 if self.widget.GetInteractor(): 2277 if self.widget.GetInteractor().GetControlKey(): 2278 self.mesh.write("vedo_clipped.vtk") 2279 printc(":save: saved mesh to vedo_clipped.vtk") 2280 2281 2282class BoxCutter(vtki.vtkBoxWidget, BaseCutter): 2283 """ 2284 Create a box widget to cut away parts of a Mesh. 2285 """ 2286 2287 def __init__( 2288 self, 2289 mesh, 2290 invert=False, 2291 can_rotate=True, 2292 can_translate=True, 2293 can_scale=True, 2294 initial_bounds=(), 2295 padding=0.025, 2296 delayed=False, 2297 c=(0.25, 0.25, 0.25), 2298 alpha=0.05, 2299 ): 2300 """ 2301 Create a box widget to cut away parts of a Mesh. 2302 2303 Arguments: 2304 mesh : (Mesh) 2305 the input mesh 2306 invert : (bool) 2307 invert the clipping plane 2308 can_rotate : (bool) 2309 enable rotation of the widget 2310 can_translate : (bool) 2311 enable translation of the widget 2312 can_scale : (bool) 2313 enable scaling of the widget 2314 initial_bounds : (list) 2315 initial bounds of the box widget 2316 padding : (float) 2317 padding space around the input mesh 2318 delayed : (bool) 2319 if True the callback is delayed until 2320 when the mouse button is released (useful for large meshes) 2321 c : (color) 2322 color of the box cutter widget 2323 alpha : (float) 2324 transparency of the cut-off part of the input mesh 2325 """ 2326 super().__init__() 2327 2328 self.mesh = mesh 2329 self.remnant = Mesh() 2330 self.remnant.name = mesh.name + "Remnant" 2331 self.remnant.pickable(False) 2332 2333 self._alpha = alpha 2334 self._keypress_id = None 2335 self._init_bounds = initial_bounds 2336 if len(self._init_bounds) == 0: 2337 self._init_bounds = mesh.bounds() 2338 else: 2339 self._init_bounds = initial_bounds 2340 2341 self._implicit_func = vtki.new("Planes") 2342 self._implicit_func.SetBounds(self._init_bounds) 2343 2344 poly = mesh.dataset 2345 self.clipper = vtki.new("ClipPolyData") 2346 self.clipper.GenerateClipScalarsOff() 2347 self.clipper.SetInputData(poly) 2348 self.clipper.SetClipFunction(self._implicit_func) 2349 self.clipper.SetInsideOut(not invert) 2350 self.clipper.GenerateClippedOutputOn() 2351 self.clipper.Update() 2352 2353 self.widget = vtki.vtkBoxWidget() 2354 2355 self.widget.SetRotationEnabled(can_rotate) 2356 self.widget.SetTranslationEnabled(can_translate) 2357 self.widget.SetScalingEnabled(can_scale) 2358 2359 self.widget.OutlineCursorWiresOn() 2360 self.widget.GetSelectedOutlineProperty().SetColor(get_color("red3")) 2361 self.widget.GetSelectedHandleProperty().SetColor(get_color("red5")) 2362 2363 self.widget.GetOutlineProperty().SetColor(c) 2364 self.widget.GetOutlineProperty().SetOpacity(1) 2365 self.widget.GetOutlineProperty().SetLineWidth(1) 2366 self.widget.GetOutlineProperty().LightingOff() 2367 2368 self.widget.GetSelectedFaceProperty().LightingOff() 2369 self.widget.GetSelectedFaceProperty().SetOpacity(0.1) 2370 2371 self.widget.SetPlaceFactor(1.0 + padding) 2372 self.widget.SetInputData(poly) 2373 self.widget.PlaceWidget() 2374 if delayed: 2375 self.widget.AddObserver("EndInteractionEvent", self._select_polygons) 2376 else: 2377 self.widget.AddObserver("InteractionEvent", self._select_polygons) 2378 2379 def _select_polygons(self, vobj, event): 2380 vobj.GetPlanes(self._implicit_func) 2381 2382 def _keypress(self, vobj, event): 2383 if vobj.GetKeySym() == "r": # reset planes 2384 self._implicit_func.SetBounds(self._init_bounds) 2385 self.widget.GetPlanes(self._implicit_func) 2386 self.widget.PlaceWidget() 2387 self.widget.GetInteractor().Render() 2388 elif vobj.GetKeySym() == "u": 2389 self.invert() 2390 self.widget.GetInteractor().Render() 2391 elif vobj.GetKeySym() == "s": # Ctrl+s to save mesh 2392 if self.widget.GetInteractor(): 2393 if self.widget.GetInteractor().GetControlKey(): 2394 self.mesh.write("vedo_clipped.vtk") 2395 printc(":save: saved mesh to vedo_clipped.vtk") 2396 2397 2398class SphereCutter(vtki.vtkSphereWidget, BaseCutter): 2399 """ 2400 Create a box widget to cut away parts of a Mesh. 2401 """ 2402 2403 def __init__( 2404 self, 2405 mesh, 2406 invert=False, 2407 can_translate=True, 2408 can_scale=True, 2409 origin=(), 2410 radius=0, 2411 res=60, 2412 delayed=False, 2413 c="white", 2414 alpha=0.05, 2415 ): 2416 """ 2417 Create a box widget to cut away parts of a Mesh. 2418 2419 Arguments: 2420 mesh : Mesh 2421 the input mesh 2422 invert : bool 2423 invert the clipping 2424 can_translate : bool 2425 enable translation of the widget 2426 can_scale : bool 2427 enable scaling of the widget 2428 origin : list 2429 initial position of the sphere widget 2430 radius : float 2431 initial radius of the sphere widget 2432 res : int 2433 resolution of the sphere widget 2434 delayed : bool 2435 if True the cutting callback is delayed until 2436 when the mouse button is released (useful for large meshes) 2437 c : color 2438 color of the box cutter widget 2439 alpha : float 2440 transparency of the cut-off part of the input mesh 2441 """ 2442 super().__init__() 2443 2444 self.mesh = mesh 2445 self.remnant = Mesh() 2446 self.remnant.name = mesh.name + "Remnant" 2447 self.remnant.pickable(False) 2448 2449 self._alpha = alpha 2450 self._keypress_id = None 2451 2452 self._implicit_func = vtki.new("Sphere") 2453 2454 if len(origin) == 3: 2455 self._implicit_func.SetCenter(origin) 2456 else: 2457 origin = mesh.center_of_mass() 2458 self._implicit_func.SetCenter(origin) 2459 2460 if radius > 0: 2461 self._implicit_func.SetRadius(radius) 2462 else: 2463 radius = mesh.average_size() * 2 2464 self._implicit_func.SetRadius(radius) 2465 2466 poly = mesh.dataset 2467 self.clipper = vtki.new("ClipPolyData") 2468 self.clipper.GenerateClipScalarsOff() 2469 self.clipper.SetInputData(poly) 2470 self.clipper.SetClipFunction(self._implicit_func) 2471 self.clipper.SetInsideOut(not invert) 2472 self.clipper.GenerateClippedOutputOn() 2473 self.clipper.Update() 2474 2475 self.widget = vtki.vtkSphereWidget() 2476 2477 self.widget.SetThetaResolution(res * 2) 2478 self.widget.SetPhiResolution(res) 2479 self.widget.SetRadius(radius) 2480 self.widget.SetCenter(origin) 2481 self.widget.SetRepresentation(2) 2482 self.widget.HandleVisibilityOff() 2483 2484 self.widget.SetTranslation(can_translate) 2485 self.widget.SetScale(can_scale) 2486 2487 self.widget.HandleVisibilityOff() 2488 self.widget.GetSphereProperty().SetColor(get_color(c)) 2489 self.widget.GetSphereProperty().SetOpacity(0.2) 2490 self.widget.GetSelectedSphereProperty().SetColor(get_color("red5")) 2491 self.widget.GetSelectedSphereProperty().SetOpacity(0.2) 2492 2493 self.widget.SetPlaceFactor(1.0) 2494 self.widget.SetInputData(poly) 2495 self.widget.PlaceWidget() 2496 if delayed: 2497 self.widget.AddObserver("EndInteractionEvent", self._select_polygons) 2498 else: 2499 self.widget.AddObserver("InteractionEvent", self._select_polygons) 2500 2501 def _select_polygons(self, vobj, event): 2502 vobj.GetSphere(self._implicit_func) 2503 2504 def _keypress(self, vobj, event): 2505 if vobj.GetKeySym() == "r": # reset planes 2506 self._implicit_func.SetBounds(self._init_bounds) 2507 self.widget.GetPlanes(self._implicit_func) 2508 self.widget.PlaceWidget() 2509 self.widget.GetInteractor().Render() 2510 elif vobj.GetKeySym() == "u": 2511 self.invert() 2512 self.widget.GetInteractor().Render() 2513 elif vobj.GetKeySym() == "s": # Ctrl+s to save mesh 2514 if self.widget.GetInteractor(): 2515 if self.widget.GetInteractor().GetControlKey(): 2516 self.mesh.write("vedo_clipped.vtk") 2517 printc(":save: saved mesh to vedo_clipped.vtk") 2518 2519 @property 2520 def center(self): 2521 """Get the center of the sphere.""" 2522 return np.array(self.widget.GetCenter()) 2523 2524 @center.setter 2525 def center(self, value): 2526 """Set the center of the sphere.""" 2527 self.widget.SetCenter(value) 2528 2529 @property 2530 def radius(self): 2531 """Get the radius of the sphere.""" 2532 return self.widget.GetRadius() 2533 2534 @radius.setter 2535 def radius(self, value): 2536 """Set the radius of the sphere.""" 2537 self.widget.SetRadius(value) 2538 2539 2540##################################################################### 2541class RendererFrame(vtki.vtkActor2D): 2542 """ 2543 Add a line around the renderer subwindow. 2544 """ 2545 2546 def __init__(self, c="k", alpha=None, lw=None, padding=None): 2547 """ 2548 Add a line around the renderer subwindow. 2549 2550 Arguments: 2551 c : (color) 2552 color of the line. 2553 alpha : (float) 2554 opacity. 2555 lw : (int) 2556 line width in pixels. 2557 padding : (int) 2558 padding in pixel units. 2559 """ 2560 2561 if lw is None: 2562 lw = settings.renderer_frame_width 2563 if lw == 0: 2564 return None 2565 2566 if alpha is None: 2567 alpha = settings.renderer_frame_alpha 2568 2569 if padding is None: 2570 padding = settings.renderer_frame_padding 2571 2572 c = get_color(c) 2573 2574 ppoints = vtki.vtkPoints() # Generate the polyline 2575 xy = 1 - padding 2576 psqr = [ 2577 [padding, padding], 2578 [padding, xy], 2579 [xy, xy], 2580 [xy, padding], 2581 [padding, padding], 2582 ] 2583 for i, pt in enumerate(psqr): 2584 ppoints.InsertPoint(i, pt[0], pt[1], 0) 2585 lines = vtki.vtkCellArray() 2586 lines.InsertNextCell(len(psqr)) 2587 for i in range(len(psqr)): 2588 lines.InsertCellPoint(i) 2589 pd = vtki.vtkPolyData() 2590 pd.SetPoints(ppoints) 2591 pd.SetLines(lines) 2592 2593 mapper = vtki.new("PolyDataMapper2D") 2594 mapper.SetInputData(pd) 2595 cs = vtki.new("Coordinate") 2596 cs.SetCoordinateSystemToNormalizedViewport() 2597 mapper.SetTransformCoordinate(cs) 2598 2599 super().__init__() 2600 2601 self.GetPositionCoordinate().SetValue(0, 0) 2602 self.GetPosition2Coordinate().SetValue(1, 1) 2603 self.SetMapper(mapper) 2604 self.GetProperty().SetColor(c) 2605 self.GetProperty().SetOpacity(alpha) 2606 self.GetProperty().SetLineWidth(lw) 2607 2608 2609##################################################################### 2610class ProgressBarWidget(vtki.vtkActor2D): 2611 """ 2612 Add a progress bar in the rendering window. 2613 """ 2614 2615 def __init__(self, n=None, c="blue5", alpha=0.8, lw=10, autohide=True): 2616 """ 2617 Add a progress bar window. 2618 2619 Arguments: 2620 n : (int) 2621 number of iterations. 2622 If None, you need to call `update(fraction)` manually. 2623 c : (color) 2624 color of the line. 2625 alpha : (float) 2626 opacity of the line. 2627 lw : (int) 2628 line width in pixels. 2629 autohide : (bool) 2630 if True, hide the progress bar when completed. 2631 """ 2632 self.n = 0 2633 self.iterations = n 2634 self.autohide = autohide 2635 2636 ppoints = vtki.vtkPoints() # Generate the line 2637 psqr = [[0, 0, 0], [1, 0, 0]] 2638 for i, pt in enumerate(psqr): 2639 ppoints.InsertPoint(i, *pt) 2640 lines = vtki.vtkCellArray() 2641 lines.InsertNextCell(len(psqr)) 2642 for i in range(len(psqr)): 2643 lines.InsertCellPoint(i) 2644 pd = vtki.vtkPolyData() 2645 pd.SetPoints(ppoints) 2646 pd.SetLines(lines) 2647 self.dataset = pd 2648 2649 mapper = vtki.new("PolyDataMapper2D") 2650 mapper.SetInputData(pd) 2651 cs = vtki.vtkCoordinate() 2652 cs.SetCoordinateSystemToNormalizedViewport() 2653 mapper.SetTransformCoordinate(cs) 2654 2655 super().__init__() 2656 2657 self.SetMapper(mapper) 2658 self.GetProperty().SetOpacity(alpha) 2659 self.GetProperty().SetColor(get_color(c)) 2660 self.GetProperty().SetLineWidth(lw * 2) 2661 2662 def lw(self, value: int) -> Self: 2663 """Set width.""" 2664 self.GetProperty().SetLineWidth(value * 2) 2665 return self 2666 2667 def c(self, color) -> Self: 2668 """Set color.""" 2669 c = get_color(color) 2670 self.GetProperty().SetColor(c) 2671 return self 2672 2673 def alpha(self, value) -> Self: 2674 """Set opacity.""" 2675 self.GetProperty().SetOpacity(value) 2676 return self 2677 2678 def update(self, fraction=None) -> Self: 2679 """Update progress bar to fraction of the window width.""" 2680 if fraction is None: 2681 if self.iterations is None: 2682 vedo.printc("Error in ProgressBarWindow: must specify iterations", c='r') 2683 return self 2684 self.n += 1 2685 fraction = self.n / self.iterations 2686 2687 if fraction >= 1 and self.autohide: 2688 fraction = 0 2689 2690 psqr = [[0, 0, 0], [fraction, 0, 0]] 2691 vpts = utils.numpy2vtk(psqr, dtype=np.float32) 2692 self.dataset.GetPoints().SetData(vpts) 2693 return self 2694 2695 def reset(self): 2696 """Reset progress bar.""" 2697 self.n = 0 2698 self.update(0) 2699 return self 2700 2701 2702##################################################################### 2703class Icon(vtki.vtkOrientationMarkerWidget): 2704 """ 2705 Add an inset icon mesh into the renderer. 2706 """ 2707 2708 def __init__(self, mesh, pos=3, size=0.08): 2709 """ 2710 Arguments: 2711 pos : (list, int) 2712 icon position in the range [1-4] indicating one of the 4 corners, 2713 or it can be a tuple (x,y) as a fraction of the renderer size. 2714 size : (float) 2715 size of the icon space as fraction of the window size. 2716 2717 Examples: 2718 - [icon.py](https://github.com/marcomusy/vedo/tree/master/examples/other/icon.py) 2719 """ 2720 super().__init__() 2721 2722 try: 2723 self.SetOrientationMarker(mesh.actor) 2724 except AttributeError: 2725 self.SetOrientationMarker(mesh) 2726 2727 if utils.is_sequence(pos): 2728 self.SetViewport(pos[0] - size, pos[1] - size, pos[0] + size, pos[1] + size) 2729 else: 2730 if pos < 2: 2731 self.SetViewport(0, 1 - 2 * size, size * 2, 1) 2732 elif pos == 2: 2733 self.SetViewport(1 - 2 * size, 1 - 2 * size, 1, 1) 2734 elif pos == 3: 2735 self.SetViewport(0, 0, size * 2, size * 2) 2736 elif pos == 4: 2737 self.SetViewport(1 - 2 * size, 0, 1, size * 2) 2738 2739 2740##################################################################### 2741def compute_visible_bounds(objs=None) -> list: 2742 """Calculate max objects bounds and sizes.""" 2743 bns = [] 2744 2745 if objs is None and vedo.plotter_instance: 2746 objs = vedo.plotter_instance.actors 2747 elif not utils.is_sequence(objs): 2748 objs = [objs] 2749 2750 actors = [ob.actor for ob in objs if hasattr(ob, "actor") and ob.actor] 2751 2752 try: 2753 # this block fails for VolumeSlice as vtkImageSlice.GetBounds() returns a pointer.. 2754 # in any case we dont need axes for that one. 2755 for a in actors: 2756 if a and a.GetUseBounds(): 2757 b = a.GetBounds() 2758 if b: 2759 bns.append(b) 2760 if bns: 2761 max_bns = np.max(bns, axis=0) 2762 min_bns = np.min(bns, axis=0) 2763 vbb = [min_bns[0], max_bns[1], min_bns[2], max_bns[3], min_bns[4], max_bns[5]] 2764 elif vedo.plotter_instance: 2765 vbb = list(vedo.plotter_instance.renderer.ComputeVisiblePropBounds()) 2766 max_bns = vbb 2767 min_bns = vbb 2768 sizes = np.array( 2769 [max_bns[1] - min_bns[0], max_bns[3] - min_bns[2], max_bns[5] - min_bns[4]] 2770 ) 2771 return [vbb, sizes, min_bns, max_bns] 2772 2773 except: 2774 return [[0, 0, 0, 0, 0, 0], [0, 0, 0], 0, 0] 2775 2776 2777##################################################################### 2778def Ruler3D( 2779 p1, 2780 p2, 2781 units_scale=1, 2782 label="", 2783 s=None, 2784 font=None, 2785 italic=0, 2786 prefix="", 2787 units="", # eg.'μm' 2788 c=(0.2, 0.1, 0.1), 2789 alpha=1, 2790 lw=1, 2791 precision=3, 2792 label_rotation=0, 2793 axis_rotation=0, 2794 tick_angle=90, 2795) -> Mesh: 2796 """ 2797 Build a 3D ruler to indicate the distance of two points p1 and p2. 2798 2799 Arguments: 2800 label : (str) 2801 alternative fixed label to be shown 2802 units_scale : (float) 2803 factor to scale units (e.g. μm to mm) 2804 s : (float) 2805 size of the label 2806 font : (str) 2807 font face. Check [available fonts here](https://vedo.embl.es/fonts). 2808 italic : (float) 2809 italicness of the font in the range [0,1] 2810 units : (str) 2811 string to be appended to the numeric value 2812 lw : (int) 2813 line width in pixel units 2814 precision : (int) 2815 nr of significant digits to be shown 2816 label_rotation : (float) 2817 initial rotation of the label around the z-axis 2818 axis_rotation : (float) 2819 initial rotation of the line around the main axis 2820 tick_angle : (float) 2821 initial rotation of the line around the main axis 2822 2823 Examples: 2824 - [goniometer.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/goniometer.py) 2825 2826 ![](https://vedo.embl.es/images/pyplot/goniometer.png) 2827 """ 2828 2829 if units_scale != 1.0 and units == "": 2830 raise ValueError( 2831 "When setting 'units_scale' to a value other than 1, " 2832 + "a 'units' arguments must be specified." 2833 ) 2834 2835 try: 2836 p1 = p1.pos() 2837 except AttributeError: 2838 pass 2839 2840 try: 2841 p2 = p2.pos() 2842 except AttributeError: 2843 pass 2844 2845 if len(p1) == 2: 2846 p1 = [p1[0], p1[1], 0.0] 2847 if len(p2) == 2: 2848 p2 = [p2[0], p2[1], 0.0] 2849 2850 p1, p2 = np.asarray(p1), np.asarray(p2) 2851 q1, q2 = [0, 0, 0], [utils.mag(p2 - p1), 0, 0] 2852 q1, q2 = np.array(q1), np.array(q2) 2853 v = q2 - q1 2854 d = utils.mag(v) * units_scale 2855 2856 pos = np.array(p1) 2857 p1 = p1 - pos 2858 p2 = p2 - pos 2859 2860 if s is None: 2861 s = d * 0.02 * (1 / units_scale) 2862 2863 if not label: 2864 label = str(d) 2865 if precision: 2866 label = utils.precision(d, precision) 2867 if prefix: 2868 label = prefix + "~" + label 2869 if units: 2870 label += "~" + units 2871 2872 lb = shapes.Text3D(label, s=s, font=font, italic=italic, justify="center") 2873 if label_rotation: 2874 lb.rotate_z(label_rotation) 2875 lb.pos((q1 + q2) / 2) 2876 2877 x0, x1 = lb.xbounds() 2878 gap = [(x1 - x0) / 2, 0, 0] 2879 pc1 = (v / 2 - gap) * 0.9 + q1 2880 pc2 = q2 - (v / 2 - gap) * 0.9 2881 2882 lc1 = shapes.Line(q1 - v / 50, pc1).lw(lw) 2883 lc2 = shapes.Line(q2 + v / 50, pc2).lw(lw) 2884 2885 zs = np.array([0, d / 50 * (1 / units_scale), 0]) 2886 ml1 = shapes.Line(-zs, zs).lw(lw) 2887 ml2 = shapes.Line(-zs, zs).lw(lw) 2888 ml1.rotate_z(tick_angle - 90).pos(q1) 2889 ml2.rotate_z(tick_angle - 90).pos(q2) 2890 2891 c1 = shapes.Circle(q1, r=d / 180 * (1 / units_scale), res=24) 2892 c2 = shapes.Circle(q2, r=d / 180 * (1 / units_scale), res=24) 2893 2894 macts = merge(lb, lc1, lc2, c1, c2, ml1, ml2) 2895 macts.c(c).alpha(alpha) 2896 macts.properties.SetLineWidth(lw) 2897 macts.properties.LightingOff() 2898 macts.actor.UseBoundsOff() 2899 macts.rotate_x(axis_rotation) 2900 macts.reorient(q2 - q1, p2 - p1) 2901 macts.pos(pos) 2902 macts.bc("tomato").pickable(False) 2903 return macts 2904 2905 2906def RulerAxes( 2907 inputobj, 2908 xtitle="", 2909 ytitle="", 2910 ztitle="", 2911 xlabel="", 2912 ylabel="", 2913 zlabel="", 2914 xpadding=0.05, 2915 ypadding=0.04, 2916 zpadding=0, 2917 font="Normografo", 2918 s=None, 2919 italic=0, 2920 units="", 2921 c=(0.2, 0, 0), 2922 alpha=1, 2923 lw=1, 2924 precision=3, 2925 label_rotation=0, 2926 xaxis_rotation=0, 2927 yaxis_rotation=0, 2928 zaxis_rotation=0, 2929 xycross=True, 2930) -> Union[Mesh, None]: 2931 """ 2932 A 3D ruler axes to indicate the sizes of the input scene or object. 2933 2934 Arguments: 2935 xtitle : (str) 2936 name of the axis or title 2937 xlabel : (str) 2938 alternative fixed label to be shown instead of the distance 2939 s : (float) 2940 size of the label 2941 font : (str) 2942 font face. Check [available fonts here](https://vedo.embl.es/fonts). 2943 italic : (float) 2944 italicness of the font in the range [0,1] 2945 units : (str) 2946 string to be appended to the numeric value 2947 lw : (int) 2948 line width in pixel units 2949 precision : (int) 2950 nr of significant digits to be shown 2951 label_rotation : (float) 2952 initial rotation of the label around the z-axis 2953 [x,y,z]axis_rotation : (float) 2954 initial rotation of the line around the main axis in degrees 2955 xycross : (bool) 2956 show two back crossing lines in the xy plane 2957 2958 Examples: 2959 - [goniometer.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/goniometer.py) 2960 """ 2961 if utils.is_sequence(inputobj): 2962 x0, x1, y0, y1, z0, z1 = inputobj 2963 else: 2964 x0, x1, y0, y1, z0, z1 = inputobj.bounds() 2965 dx, dy, dz = (y1 - y0) * xpadding, (x1 - x0) * ypadding, (y1 - y0) * zpadding 2966 d = np.sqrt((y1 - y0) ** 2 + (x1 - x0) ** 2 + (z1 - z0) ** 2) 2967 2968 if not d: 2969 return None 2970 2971 if s is None: 2972 s = d / 75 2973 2974 acts, rx, ry = [], None, None 2975 if xtitle is not None and (x1 - x0) / d > 0.1: 2976 rx = Ruler3D( 2977 [x0, y0 - dx, z0], 2978 [x1, y0 - dx, z0], 2979 s=s, 2980 font=font, 2981 precision=precision, 2982 label_rotation=label_rotation, 2983 axis_rotation=xaxis_rotation, 2984 lw=lw, 2985 italic=italic, 2986 prefix=xtitle, 2987 label=xlabel, 2988 units=units, 2989 ) 2990 acts.append(rx) 2991 2992 if ytitle is not None and (y1 - y0) / d > 0.1: 2993 ry = Ruler3D( 2994 [x1 + dy, y0, z0], 2995 [x1 + dy, y1, z0], 2996 s=s, 2997 font=font, 2998 precision=precision, 2999 label_rotation=label_rotation, 3000 axis_rotation=yaxis_rotation, 3001 lw=lw, 3002 italic=italic, 3003 prefix=ytitle, 3004 label=ylabel, 3005 units=units, 3006 ) 3007 acts.append(ry) 3008 3009 if ztitle is not None and (z1 - z0) / d > 0.1: 3010 rz = Ruler3D( 3011 [x0 - dy, y0 + dz, z0], 3012 [x0 - dy, y0 + dz, z1], 3013 s=s, 3014 font=font, 3015 precision=precision, 3016 label_rotation=label_rotation, 3017 axis_rotation=zaxis_rotation + 90, 3018 lw=lw, 3019 italic=italic, 3020 prefix=ztitle, 3021 label=zlabel, 3022 units=units, 3023 ) 3024 acts.append(rz) 3025 3026 if xycross and rx and ry: 3027 lx = shapes.Line([x0, y0, z0], [x0, y1 + dx, z0]) 3028 ly = shapes.Line([x0 - dy, y1, z0], [x1, y1, z0]) 3029 d = min((x1 - x0), (y1 - y0)) / 200 3030 cxy = shapes.Circle([x0, y1, z0], r=d, res=15) 3031 acts.extend([lx, ly, cxy]) 3032 3033 macts = merge(acts) 3034 if not macts: 3035 return None 3036 macts.c(c).alpha(alpha).bc("t") 3037 macts.actor.UseBoundsOff() 3038 macts.actor.PickableOff() 3039 return macts 3040 3041 3042##################################################################### 3043class Ruler2D(vtki.vtkAxisActor2D): 3044 """ 3045 Create a ruler with tick marks, labels and a title. 3046 """ 3047 3048 def __init__( 3049 self, 3050 lw=2, 3051 ticks=True, 3052 labels=False, 3053 c="k", 3054 alpha=1, 3055 title="", 3056 font="Calco", 3057 font_size=24, 3058 bc=None, 3059 ): 3060 """ 3061 Create a ruler with tick marks, labels and a title. 3062 3063 Ruler2D is a 2D actor; that is, it is drawn on the overlay 3064 plane and is not occluded by 3D geometry. 3065 To use this class, specify two points defining the start and end 3066 with update_points() as 3D points. 3067 3068 This class decides decides how to create reasonable tick 3069 marks and labels. 3070 3071 Labels are drawn on the "right" side of the axis. 3072 The "right" side is the side of the axis on the right. 3073 The way the labels and title line up with the axis and tick marks 3074 depends on whether the line is considered horizontal or vertical. 3075 3076 Arguments: 3077 lw : (int) 3078 width of the line in pixel units 3079 ticks : (bool) 3080 control if drawing the tick marks 3081 labels : (bool) 3082 control if drawing the numeric labels 3083 c : (color) 3084 color of the object 3085 alpha : (float) 3086 opacity of the object 3087 title : (str) 3088 title of the ruler 3089 font : (str) 3090 font face name. Check [available fonts here](https://vedo.embl.es/fonts). 3091 font_size : (int) 3092 font size 3093 bc : (color) 3094 background color of the title 3095 3096 Example: 3097 ```python 3098 from vedo import * 3099 plt = Plotter(axes=1, interactive=False) 3100 plt.show(Cube()) 3101 rul = Ruler2D() 3102 rul.set_points([0,0,0], [0.5,0.5,0.5]) 3103 plt.add(rul) 3104 plt.interactive().close() 3105 ``` 3106 ![](https://vedo.embl.es/images/feats/dist_tool.png) 3107 """ 3108 super().__init__() 3109 3110 plt = vedo.plotter_instance 3111 if not plt: 3112 vedo.logger.error("Ruler2D need to initialize Plotter first.") 3113 raise RuntimeError() 3114 3115 self.p0 = [0, 0, 0] 3116 self.p1 = [0, 0, 0] 3117 self.distance = 0 3118 self.title = title 3119 3120 prop = self.GetProperty() 3121 tprop = self.GetTitleTextProperty() 3122 3123 self.SetTitle(title) 3124 self.SetNumberOfLabels(9) 3125 3126 if not font: 3127 font = settings.default_font 3128 if font.lower() == "courier": 3129 tprop.SetFontFamilyToCourier() 3130 elif font.lower() == "times": 3131 tprop.SetFontFamilyToTimes() 3132 elif font.lower() == "arial": 3133 tprop.SetFontFamilyToArial() 3134 else: 3135 tprop.SetFontFamily(vtki.VTK_FONT_FILE) 3136 tprop.SetFontFile(utils.get_font_path(font)) 3137 tprop.SetFontSize(font_size) 3138 tprop.BoldOff() 3139 tprop.ItalicOff() 3140 tprop.ShadowOff() 3141 tprop.SetColor(get_color(c)) 3142 tprop.SetOpacity(alpha) 3143 if bc is not None: 3144 bc = get_color(bc) 3145 tprop.SetBackgroundColor(bc) 3146 tprop.SetBackgroundOpacity(alpha) 3147 3148 lprop = vtki.vtkTextProperty() 3149 lprop.ShallowCopy(tprop) 3150 self.SetLabelTextProperty(lprop) 3151 3152 self.SetLabelFormat("%0.3g") 3153 self.SetTickVisibility(ticks) 3154 self.SetLabelVisibility(labels) 3155 prop.SetLineWidth(lw) 3156 prop.SetColor(get_color(c)) 3157 3158 self.renderer = plt.renderer 3159 self.cid = plt.interactor.AddObserver("RenderEvent", self._update_viz, 1.0) 3160 3161 def color(self, c) -> Self: 3162 """Assign a new color.""" 3163 c = get_color(c) 3164 self.GetTitleTextProperty().SetColor(c) 3165 self.GetLabelTextProperty().SetColor(c) 3166 self.GetProperty().SetColor(c) 3167 return self 3168 3169 def off(self) -> None: 3170 """Switch off the ruler completely.""" 3171 self.renderer.RemoveObserver(self.cid) 3172 self.renderer.RemoveActor(self) 3173 3174 def set_points(self, p0, p1) -> Self: 3175 """Set new values for the ruler start and end points.""" 3176 self.p0 = np.asarray(p0) 3177 self.p1 = np.asarray(p1) 3178 self._update_viz(0, 0) 3179 return self 3180 3181 def _update_viz(self, evt, name) -> None: 3182 ren = self.renderer 3183 view_size = np.array(ren.GetSize()) 3184 3185 ren.SetWorldPoint(*self.p0, 1) 3186 ren.WorldToDisplay() 3187 disp_point1 = ren.GetDisplayPoint()[:2] 3188 disp_point1 = np.array(disp_point1) / view_size 3189 3190 ren.SetWorldPoint(*self.p1, 1) 3191 ren.WorldToDisplay() 3192 disp_point2 = ren.GetDisplayPoint()[:2] 3193 disp_point2 = np.array(disp_point2) / view_size 3194 3195 self.SetPoint1(*disp_point1) 3196 self.SetPoint2(*disp_point2) 3197 self.distance = np.linalg.norm(self.p1 - self.p0) 3198 self.SetRange(0.0, float(self.distance)) 3199 if not self.title: 3200 self.SetTitle(utils.precision(self.distance, 3)) 3201 3202 3203##################################################################### 3204class DistanceTool(Group): 3205 """ 3206 Create a tool to measure the distance between two clicked points. 3207 """ 3208 3209 def __init__(self, plotter=None, c="k", lw=2): 3210 """ 3211 Create a tool to measure the distance between two clicked points. 3212 3213 Example: 3214 ```python 3215 from vedo import * 3216 mesh = ParametricShape("RandomHills").c("red5") 3217 plt = Plotter(axes=1) 3218 dtool = DistanceTool() 3219 dtool.on() 3220 plt.show(mesh, dtool) 3221 dtool.off() 3222 ``` 3223 ![](https://vedo.embl.es/images/feats/dist_tool.png) 3224 """ 3225 super().__init__() 3226 3227 self.p0 = [0, 0, 0] 3228 self.p1 = [0, 0, 0] 3229 self.distance = 0 3230 if plotter is None: 3231 plotter = vedo.plotter_instance 3232 self.plotter = plotter 3233 self.callback = None 3234 self.cid = None 3235 self.color = c 3236 self.linewidth = lw 3237 self.toggle = True 3238 self.ruler = None 3239 self.title = "" 3240 3241 def on(self) -> Self: 3242 """Switch tool on.""" 3243 self.cid = self.plotter.add_callback("click", self._onclick) 3244 self.VisibilityOn() 3245 self.plotter.render() 3246 return self 3247 3248 def off(self) -> None: 3249 """Switch tool off.""" 3250 self.plotter.remove_callback(self.cid) 3251 self.VisibilityOff() 3252 self.ruler.off() 3253 self.plotter.render() 3254 3255 def _onclick(self, event): 3256 if not event.actor: 3257 return 3258 3259 self.clear() 3260 3261 acts = [] 3262 if self.toggle: 3263 self.p0 = event.picked3d 3264 acts.append(Point(self.p0, c=self.color)) 3265 else: 3266 self.p1 = event.picked3d 3267 self.distance = np.linalg.norm(self.p1 - self.p0) 3268 acts.append(Point(self.p0, c=self.color)) 3269 acts.append(Point(self.p1, c=self.color)) 3270 self.ruler = Ruler2D(c=self.color) 3271 self.ruler.set_points(self.p0, self.p1) 3272 acts.append(self.ruler) 3273 3274 if self.callback is not None: 3275 self.callback(event) 3276 3277 for a in acts: 3278 try: 3279 self += a.actor 3280 except AttributeError: 3281 self += a 3282 self.toggle = not self.toggle 3283 3284 3285##################################################################### 3286def Axes( 3287 obj=None, 3288 xtitle='x', ytitle='y', ztitle='z', 3289 xrange=None, yrange=None, zrange=None, 3290 c=None, 3291 number_of_divisions=None, 3292 digits=None, 3293 limit_ratio=0.04, 3294 title_depth=0, 3295 title_font="", # grab settings.default_font 3296 text_scale=1.0, 3297 x_values_and_labels=None, y_values_and_labels=None, z_values_and_labels=None, 3298 htitle="", 3299 htitle_size=0.03, 3300 htitle_font=None, 3301 htitle_italic=False, 3302 htitle_color=None, htitle_backface_color=None, 3303 htitle_justify='bottom-left', 3304 htitle_rotation=0, 3305 htitle_offset=(0, 0.01, 0), 3306 xtitle_position=0.95, ytitle_position=0.95, ztitle_position=0.95, 3307 # xtitle_offset can be a list (dx,dy,dz) 3308 xtitle_offset=0.025, ytitle_offset=0.0275, ztitle_offset=0.02, 3309 xtitle_justify=None, ytitle_justify=None, ztitle_justify=None, 3310 # xtitle_rotation can be a list (rx,ry,rz) 3311 xtitle_rotation=0, ytitle_rotation=0, ztitle_rotation=0, 3312 xtitle_box=False, ytitle_box=False, 3313 xtitle_size=0.025, ytitle_size=0.025, ztitle_size=0.025, 3314 xtitle_color=None, ytitle_color=None, ztitle_color=None, 3315 xtitle_backface_color=None, ytitle_backface_color=None, ztitle_backface_color=None, 3316 xtitle_italic=0, ytitle_italic=0, ztitle_italic=0, 3317 grid_linewidth=1, 3318 xygrid=True, yzgrid=False, zxgrid=False, 3319 xygrid2=False, yzgrid2=False, zxgrid2=False, 3320 xygrid_transparent=False, yzgrid_transparent=False, zxgrid_transparent=False, 3321 xygrid2_transparent=False, yzgrid2_transparent=False, zxgrid2_transparent=False, 3322 xyplane_color=None, yzplane_color=None, zxplane_color=None, 3323 xygrid_color=None, yzgrid_color=None, zxgrid_color=None, 3324 xyalpha=0.075, yzalpha=0.075, zxalpha=0.075, 3325 xyframe_line=None, yzframe_line=None, zxframe_line=None, 3326 xyframe_color=None, yzframe_color=None, zxframe_color=None, 3327 axes_linewidth=1, 3328 xline_color=None, yline_color=None, zline_color=None, 3329 xhighlight_zero=False, yhighlight_zero=False, zhighlight_zero=False, 3330 xhighlight_zero_color='red4', yhighlight_zero_color='green4', zhighlight_zero_color='blue4', 3331 show_ticks=True, 3332 xtick_length=0.015, ytick_length=0.015, ztick_length=0.015, 3333 xtick_thickness=0.0025, ytick_thickness=0.0025, ztick_thickness=0.0025, 3334 xminor_ticks=1, yminor_ticks=1, zminor_ticks=1, 3335 tip_size=None, 3336 label_font="", # grab settings.default_font 3337 xlabel_color=None, ylabel_color=None, zlabel_color=None, 3338 xlabel_backface_color=None, ylabel_backface_color=None, zlabel_backface_color=None, 3339 xlabel_size=0.016, ylabel_size=0.016, zlabel_size=0.016, 3340 xlabel_offset=0.8, ylabel_offset=0.8, zlabel_offset=0.8, # each can be a list (dx,dy,dz) 3341 xlabel_justify=None, ylabel_justify=None, zlabel_justify=None, 3342 xlabel_rotation=0, ylabel_rotation=0, zlabel_rotation=0, # each can be a list (rx,ry,rz) 3343 xaxis_rotation=0, yaxis_rotation=0, zaxis_rotation=0, # rotate all elements around axis 3344 xyshift=0, yzshift=0, zxshift=0, 3345 xshift_along_y=0, xshift_along_z=0, 3346 yshift_along_x=0, yshift_along_z=0, 3347 zshift_along_x=0, zshift_along_y=0, 3348 x_use_bounds=True, y_use_bounds=True, z_use_bounds=False, 3349 x_inverted=False, y_inverted=False, z_inverted=False, 3350 use_global=False, 3351 tol=0.001, 3352 ) -> Union[Assembly, None]: 3353 """ 3354 Draw axes for the input object. 3355 Check [available fonts here](https://vedo.embl.es/fonts). 3356 3357 Returns an `vedo.Assembly` object. 3358 3359 Parameters 3360 ---------- 3361 3362 - `xtitle`, ['x'], x-axis title text 3363 - `xrange`, [None], x-axis range in format (xmin, ymin), default is automatic. 3364 - `number_of_divisions`, [None], approximate number of divisions on the longest axis 3365 - `axes_linewidth`, [1], width of the axes lines 3366 - `grid_linewidth`, [1], width of the grid lines 3367 - `title_depth`, [0], extrusion fractional depth of title text 3368 - `x_values_and_labels` [], assign custom tick positions and labels [(pos1, label1), ...] 3369 - `xygrid`, [True], show a gridded wall on plane xy 3370 - `yzgrid`, [True], show a gridded wall on plane yz 3371 - `zxgrid`, [True], show a gridded wall on plane zx 3372 - `yzgrid2`, [False], show yz plane on opposite side of the bounding box 3373 - `zxgrid2`, [False], show zx plane on opposite side of the bounding box 3374 - `xygrid_transparent` [False], make grid plane completely transparent 3375 - `xygrid2_transparent` [False], make grid plane completely transparent on opposite side box 3376 - `xyplane_color`, ['None'], color of the plane 3377 - `xygrid_color`, ['None'], grid line color 3378 - `xyalpha`, [0.15], grid plane opacity 3379 - `xyframe_line`, [0], add a frame for the plane, use value as the thickness 3380 - `xyframe_color`, [None], color for the frame of the plane 3381 - `show_ticks`, [True], show major ticks 3382 - `digits`, [None], use this number of significant digits in scientific notation 3383 - `title_font`, [''], font for axes titles 3384 - `label_font`, [''], font for numeric labels 3385 - `text_scale`, [1.0], global scaling factor for all text elements (titles, labels) 3386 - `htitle`, [''], header title 3387 - `htitle_size`, [0.03], header title size 3388 - `htitle_font`, [None], header font (defaults to `title_font`) 3389 - `htitle_italic`, [True], header font is italic 3390 - `htitle_color`, [None], header title color (defaults to `xtitle_color`) 3391 - `htitle_backface_color`, [None], header title color on its backface 3392 - `htitle_justify`, ['bottom-center'], origin of the title justification 3393 - `htitle_offset`, [(0,0.01,0)], control offsets of header title in x, y and z 3394 - `xtitle_position`, [0.32], title fractional positions along axis 3395 - `xtitle_offset`, [0.05], title fractional offset distance from axis line, can be a list 3396 - `xtitle_justify`, [None], choose the origin of the bounding box of title 3397 - `xtitle_rotation`, [0], add a rotation of the axis title, can be a list (rx,ry,rz) 3398 - `xtitle_box`, [False], add a box around title text 3399 - `xline_color`, [automatic], color of the x-axis 3400 - `xtitle_color`, [automatic], color of the axis title 3401 - `xtitle_backface_color`, [None], color of axis title on its backface 3402 - `xtitle_size`, [0.025], size of the axis title 3403 - `xtitle_italic`, [0], a bool or float to make the font italic 3404 - `xhighlight_zero`, [True], draw a line highlighting zero position if in range 3405 - `xhighlight_zero_color`, [auto], color of the line highlighting the zero position 3406 - `xtick_length`, [0.005], radius of the major ticks 3407 - `xtick_thickness`, [0.0025], thickness of the major ticks along their axis 3408 - `xminor_ticks`, [1], number of minor ticks between two major ticks 3409 - `xlabel_color`, [automatic], color of numeric labels and ticks 3410 - `xlabel_backface_color`, [auto], back face color of numeric labels and ticks 3411 - `xlabel_size`, [0.015], size of the numeric labels along axis 3412 - `xlabel_rotation`, [0,list], numeric labels rotation (can be a list of 3 rotations) 3413 - `xlabel_offset`, [0.8,list], offset of the numeric labels (can be a list of 3 offsets) 3414 - `xlabel_justify`, [None], choose the origin of the bounding box of labels 3415 - `xaxis_rotation`, [0], rotate the X axis elements (ticks and labels) around this same axis 3416 - `xyshift` [0.0], slide the xy-plane along z (the range is [0,1]) 3417 - `xshift_along_y` [0.0], slide x-axis along the y-axis (the range is [0,1]) 3418 - `tip_size`, [0.01], size of the arrow tip as a fraction of the bounding box diagonal 3419 - `limit_ratio`, [0.04], below this ratio don't plot smaller axis 3420 - `x_use_bounds`, [True], keep into account space occupied by labels when setting camera 3421 - `x_inverted`, [False], invert labels order and direction (only visually!) 3422 - `use_global`, [False], try to compute the global bounding box of visible actors 3423 3424 Example: 3425 ```python 3426 from vedo import Axes, Box, show 3427 box = Box(pos=(1,2,3), length=8, width=9, height=7).alpha(0.1) 3428 axs = Axes(box, c='k') # returns an Assembly object 3429 for a in axs.unpack(): 3430 print(a.name) 3431 show(box, axs).close() 3432 ``` 3433 ![](https://vedo.embl.es/images/feats/axes1.png) 3434 3435 Examples: 3436 - [custom_axes1.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/custom_axes1.py) 3437 - [custom_axes2.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/custom_axes2.py) 3438 - [custom_axes3.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/custom_axes3.py) 3439 - [custom_axes4.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/custom_axes4.py) 3440 3441 ![](https://vedo.embl.es/images/pyplot/customAxes3.png) 3442 """ 3443 if not title_font: 3444 title_font = vedo.settings.default_font 3445 if not label_font: 3446 label_font = vedo.settings.default_font 3447 3448 if c is None: # automatic black or white 3449 c = (0.1, 0.1, 0.1) 3450 plt = vedo.plotter_instance 3451 if plt and plt.renderer: 3452 bgcol = plt.renderer.GetBackground() 3453 else: 3454 bgcol = (1, 1, 1) 3455 if np.sum(bgcol) < 1.5: 3456 c = (0.9, 0.9, 0.9) 3457 else: 3458 c = get_color(c) 3459 3460 # Check if obj has bounds, if so use those 3461 if obj is not None: 3462 try: 3463 bb = obj.bounds() 3464 except AttributeError: 3465 try: 3466 bb = obj.GetBounds() 3467 if xrange is None: xrange = (bb[0], bb[1]) 3468 if yrange is None: yrange = (bb[2], bb[3]) 3469 if zrange is None: zrange = (bb[4], bb[5]) 3470 obj = None # dont need it anymore 3471 except AttributeError: 3472 pass 3473 if utils.is_sequence(obj) and len(obj) == 6 and utils.is_number(obj[0]): 3474 # passing a list of numeric bounds 3475 if xrange is None: xrange = (obj[0], obj[1]) 3476 if yrange is None: yrange = (obj[2], obj[3]) 3477 if zrange is None: zrange = (obj[4], obj[5]) 3478 3479 if use_global: 3480 vbb, drange, min_bns, max_bns = compute_visible_bounds() 3481 else: 3482 if obj is not None: 3483 vbb, drange, min_bns, max_bns = compute_visible_bounds(obj) 3484 else: 3485 vbb = np.zeros(6) 3486 drange = np.zeros(3) 3487 if zrange is None: 3488 zrange = (0, 0) 3489 if xrange is None or yrange is None: 3490 vedo.logger.error("in Axes() must specify axes ranges!") 3491 return None ########################################### 3492 3493 if xrange is not None: 3494 if xrange[1] < xrange[0]: 3495 x_inverted = True 3496 xrange = [xrange[1], xrange[0]] 3497 vbb[0], vbb[1] = xrange 3498 drange[0] = vbb[1] - vbb[0] 3499 min_bns = vbb 3500 max_bns = vbb 3501 if yrange is not None: 3502 if yrange[1] < yrange[0]: 3503 y_inverted = True 3504 yrange = [yrange[1], yrange[0]] 3505 vbb[2], vbb[3] = yrange 3506 drange[1] = vbb[3] - vbb[2] 3507 min_bns = vbb 3508 max_bns = vbb 3509 if zrange is not None: 3510 if zrange[1] < zrange[0]: 3511 z_inverted = True 3512 zrange = [zrange[1], zrange[0]] 3513 vbb[4], vbb[5] = zrange 3514 drange[2] = vbb[5] - vbb[4] 3515 min_bns = vbb 3516 max_bns = vbb 3517 3518 drangemax = max(drange) 3519 if not drangemax: 3520 return None 3521 3522 if drange[0] / drangemax < limit_ratio: 3523 drange[0] = 0 3524 xtitle = "" 3525 if drange[1] / drangemax < limit_ratio: 3526 drange[1] = 0 3527 ytitle = "" 3528 if drange[2] / drangemax < limit_ratio: 3529 drange[2] = 0 3530 ztitle = "" 3531 3532 x0, x1, y0, y1, z0, z1 = vbb 3533 dx, dy, dz = drange 3534 3535 gscale = np.sqrt(dx * dx + dy * dy + dz * dz) * 0.75 3536 3537 if not xyplane_color: xyplane_color = c 3538 if not yzplane_color: yzplane_color = c 3539 if not zxplane_color: zxplane_color = c 3540 if not xygrid_color: xygrid_color = c 3541 if not yzgrid_color: yzgrid_color = c 3542 if not zxgrid_color: zxgrid_color = c 3543 if not xtitle_color: xtitle_color = c 3544 if not ytitle_color: ytitle_color = c 3545 if not ztitle_color: ztitle_color = c 3546 if not xline_color: xline_color = c 3547 if not yline_color: yline_color = c 3548 if not zline_color: zline_color = c 3549 if not xlabel_color: xlabel_color = xline_color 3550 if not ylabel_color: ylabel_color = yline_color 3551 if not zlabel_color: zlabel_color = zline_color 3552 3553 if tip_size is None: 3554 tip_size = 0.005 * gscale 3555 if not ztitle: 3556 tip_size = 0 # switch off in xy 2d 3557 3558 ndiv = 4 3559 if not ztitle or not ytitle or not xtitle: # make more default ticks if 2D 3560 ndiv = 6 3561 if not ztitle: 3562 if xyframe_line is None: 3563 xyframe_line = True 3564 if tip_size is None: 3565 tip_size = False 3566 3567 if utils.is_sequence(number_of_divisions): 3568 rx, ry, rz = number_of_divisions 3569 else: 3570 if not number_of_divisions: 3571 number_of_divisions = ndiv 3572 3573 rx, ry, rz = np.ceil(drange / drangemax * number_of_divisions).astype(int) 3574 3575 if xtitle: 3576 xticks_float, xticks_str = utils.make_ticks(x0, x1, rx, x_values_and_labels, digits) 3577 xticks_float = xticks_float * dx 3578 if x_inverted: 3579 xticks_float = np.flip(-(xticks_float - xticks_float[-1])) 3580 xticks_str = list(reversed(xticks_str)) 3581 xticks_str[-1] = "" 3582 xhighlight_zero = False 3583 if ytitle: 3584 yticks_float, yticks_str = utils.make_ticks(y0, y1, ry, y_values_and_labels, digits) 3585 yticks_float = yticks_float * dy 3586 if y_inverted: 3587 yticks_float = np.flip(-(yticks_float - yticks_float[-1])) 3588 yticks_str = list(reversed(yticks_str)) 3589 yticks_str[-1] = "" 3590 yhighlight_zero = False 3591 if ztitle: 3592 zticks_float, zticks_str = utils.make_ticks(z0, z1, rz, z_values_and_labels, digits) 3593 zticks_float = zticks_float * dz 3594 if z_inverted: 3595 zticks_float = np.flip(-(zticks_float - zticks_float[-1])) 3596 zticks_str = list(reversed(zticks_str)) 3597 zticks_str[-1] = "" 3598 zhighlight_zero = False 3599 3600 ################################################ axes lines 3601 lines = [] 3602 if xtitle: 3603 axlinex = shapes.Line([0,0,0], [dx,0,0], c=xline_color, lw=axes_linewidth) 3604 axlinex.shift([0, zxshift*dy + xshift_along_y*dy, xyshift*dz + xshift_along_z*dz]) 3605 axlinex.name = 'xAxis' 3606 lines.append(axlinex) 3607 if ytitle: 3608 axliney = shapes.Line([0,0,0], [0,dy,0], c=yline_color, lw=axes_linewidth) 3609 axliney.shift([yzshift*dx + yshift_along_x*dx, 0, xyshift*dz + yshift_along_z*dz]) 3610 axliney.name = 'yAxis' 3611 lines.append(axliney) 3612 if ztitle: 3613 axlinez = shapes.Line([0,0,0], [0,0,dz], c=zline_color, lw=axes_linewidth) 3614 axlinez.shift([yzshift*dx + zshift_along_x*dx, zxshift*dy + zshift_along_y*dy, 0]) 3615 axlinez.name = 'zAxis' 3616 lines.append(axlinez) 3617 3618 ################################################ grid planes 3619 # all shapes have a name to keep track of them in the Assembly 3620 # if user wants to unpack it 3621 grids = [] 3622 if xygrid and xtitle and ytitle: 3623 if not xygrid_transparent: 3624 gxy = shapes.Grid(s=(xticks_float, yticks_float)) 3625 gxy.alpha(xyalpha).c(xyplane_color).lw(0) 3626 if xyshift: gxy.shift([0,0,xyshift*dz]) 3627 elif tol: gxy.shift([0,0,-tol*gscale]) 3628 gxy.name = "xyGrid" 3629 grids.append(gxy) 3630 if grid_linewidth: 3631 gxy_lines = shapes.Grid(s=(xticks_float, yticks_float)) 3632 gxy_lines.c(xyplane_color).lw(grid_linewidth).alpha(xyalpha) 3633 if xyshift: gxy_lines.shift([0,0,xyshift*dz]) 3634 elif tol: gxy_lines.shift([0,0,-tol*gscale]) 3635 gxy_lines.name = "xyGridLines" 3636 grids.append(gxy_lines) 3637 3638 if yzgrid and ytitle and ztitle: 3639 if not yzgrid_transparent: 3640 gyz = shapes.Grid(s=(zticks_float, yticks_float)) 3641 gyz.alpha(yzalpha).c(yzplane_color).lw(0).rotate_y(-90) 3642 if yzshift: gyz.shift([yzshift*dx,0,0]) 3643 elif tol: gyz.shift([-tol*gscale,0,0]) 3644 gyz.name = "yzGrid" 3645 grids.append(gyz) 3646 if grid_linewidth: 3647 gyz_lines = shapes.Grid(s=(zticks_float, yticks_float)) 3648 gyz_lines.c(yzplane_color).lw(grid_linewidth).alpha(yzalpha).rotate_y(-90) 3649 if yzshift: gyz_lines.shift([yzshift*dx,0,0]) 3650 elif tol: gyz_lines.shift([-tol*gscale,0,0]) 3651 gyz_lines.name = "yzGridLines" 3652 grids.append(gyz_lines) 3653 3654 if zxgrid and ztitle and xtitle: 3655 if not zxgrid_transparent: 3656 gzx = shapes.Grid(s=(xticks_float, zticks_float)) 3657 gzx.alpha(zxalpha).c(zxplane_color).lw(0).rotate_x(90) 3658 if zxshift: gzx.shift([0,zxshift*dy,0]) 3659 elif tol: gzx.shift([0,-tol*gscale,0]) 3660 gzx.name = "zxGrid" 3661 grids.append(gzx) 3662 if grid_linewidth: 3663 gzx_lines = shapes.Grid(s=(xticks_float, zticks_float)) 3664 gzx_lines.c(zxplane_color).lw(grid_linewidth).alpha(zxalpha).rotate_x(90) 3665 if zxshift: gzx_lines.shift([0,zxshift*dy,0]) 3666 elif tol: gzx_lines.shift([0,-tol*gscale,0]) 3667 gzx_lines.name = "zxGridLines" 3668 grids.append(gzx_lines) 3669 3670 # Grid2 3671 if xygrid2 and xtitle and ytitle: 3672 if not xygrid2_transparent: 3673 gxy2 = shapes.Grid(s=(xticks_float, yticks_float)).z(dz) 3674 gxy2.alpha(xyalpha).c(xyplane_color).lw(0) 3675 gxy2.shift([0, tol * gscale, 0]) 3676 gxy2.name = "xyGrid2" 3677 grids.append(gxy2) 3678 if grid_linewidth: 3679 gxy2_lines = shapes.Grid(s=(xticks_float, yticks_float)).z(dz) 3680 gxy2_lines.c(xyplane_color).lw(grid_linewidth).alpha(xyalpha) 3681 gxy2_lines.shift([0, tol * gscale, 0]) 3682 gxy2_lines.name = "xygrid2Lines" 3683 grids.append(gxy2_lines) 3684 3685 if yzgrid2 and ytitle and ztitle: 3686 if not yzgrid2_transparent: 3687 gyz2 = shapes.Grid(s=(zticks_float, yticks_float)) 3688 gyz2.alpha(yzalpha).c(yzplane_color).lw(0) 3689 gyz2.rotate_y(-90).x(dx).shift([tol * gscale, 0, 0]) 3690 gyz2.name = "yzGrid2" 3691 grids.append(gyz2) 3692 if grid_linewidth: 3693 gyz2_lines = shapes.Grid(s=(zticks_float, yticks_float)) 3694 gyz2_lines.c(yzplane_color).lw(grid_linewidth).alpha(yzalpha) 3695 gyz2_lines.rotate_y(-90).x(dx).shift([tol * gscale, 0, 0]) 3696 gyz2_lines.name = "yzGrid2Lines" 3697 grids.append(gyz2_lines) 3698 3699 if zxgrid2 and ztitle and xtitle: 3700 if not zxgrid2_transparent: 3701 gzx2 = shapes.Grid(s=(xticks_float, zticks_float)) 3702 gzx2.alpha(zxalpha).c(zxplane_color).lw(0) 3703 gzx2.rotate_x(90).y(dy).shift([0, tol * gscale, 0]) 3704 gzx2.name = "zxGrid2" 3705 grids.append(gzx2) 3706 if grid_linewidth: 3707 gzx2_lines = shapes.Grid(s=(xticks_float, zticks_float)) 3708 gzx2_lines.c(zxplane_color).lw(grid_linewidth).alpha(zxalpha) 3709 gzx2_lines.rotate_x(90).y(dy).shift([0, tol * gscale, 0]) 3710 gzx2_lines.name = "zxGrid2Lines" 3711 grids.append(gzx2_lines) 3712 3713 ################################################ frame lines 3714 framelines = [] 3715 if xyframe_line and xtitle and ytitle: 3716 if not xyframe_color: 3717 xyframe_color = xygrid_color 3718 frxy = shapes.Line( 3719 [[0, dy, 0], [dx, dy, 0], [dx, 0, 0], [0, 0, 0], [0, dy, 0]], 3720 c=xyframe_color, 3721 lw=xyframe_line, 3722 ) 3723 frxy.shift([0, 0, xyshift * dz]) 3724 frxy.name = "xyFrameLine" 3725 framelines.append(frxy) 3726 if yzframe_line and ytitle and ztitle: 3727 if not yzframe_color: 3728 yzframe_color = yzgrid_color 3729 fryz = shapes.Line( 3730 [[0, 0, dz], [0, dy, dz], [0, dy, 0], [0, 0, 0], [0, 0, dz]], 3731 c=yzframe_color, 3732 lw=yzframe_line, 3733 ) 3734 fryz.shift([yzshift * dx, 0, 0]) 3735 fryz.name = "yzFrameLine" 3736 framelines.append(fryz) 3737 if zxframe_line and ztitle and xtitle: 3738 if not zxframe_color: 3739 zxframe_color = zxgrid_color 3740 frzx = shapes.Line( 3741 [[0, 0, dz], [dx, 0, dz], [dx, 0, 0], [0, 0, 0], [0, 0, dz]], 3742 c=zxframe_color, 3743 lw=zxframe_line, 3744 ) 3745 frzx.shift([0, zxshift * dy, 0]) 3746 frzx.name = "zxFrameLine" 3747 framelines.append(frzx) 3748 3749 ################################################ zero lines highlights 3750 highlights = [] 3751 if xygrid and xtitle and ytitle: 3752 if xhighlight_zero and min_bns[0] <= 0 and max_bns[1] > 0: 3753 xhl = -min_bns[0] 3754 hxy = shapes.Line([xhl, 0, 0], [xhl, dy, 0], c=xhighlight_zero_color) 3755 hxy.alpha(np.sqrt(xyalpha)).lw(grid_linewidth * 2) 3756 hxy.shift([0, 0, xyshift * dz]) 3757 hxy.name = "xyHighlightZero" 3758 highlights.append(hxy) 3759 if yhighlight_zero and min_bns[2] <= 0 and max_bns[3] > 0: 3760 yhl = -min_bns[2] 3761 hyx = shapes.Line([0, yhl, 0], [dx, yhl, 0], c=yhighlight_zero_color) 3762 hyx.alpha(np.sqrt(yzalpha)).lw(grid_linewidth * 2) 3763 hyx.shift([0, 0, xyshift * dz]) 3764 hyx.name = "yxHighlightZero" 3765 highlights.append(hyx) 3766 3767 if yzgrid and ytitle and ztitle: 3768 if yhighlight_zero and min_bns[2] <= 0 and max_bns[3] > 0: 3769 yhl = -min_bns[2] 3770 hyz = shapes.Line([0, yhl, 0], [0, yhl, dz], c=yhighlight_zero_color) 3771 hyz.alpha(np.sqrt(yzalpha)).lw(grid_linewidth * 2) 3772 hyz.shift([yzshift * dx, 0, 0]) 3773 hyz.name = "yzHighlightZero" 3774 highlights.append(hyz) 3775 if zhighlight_zero and min_bns[4] <= 0 and max_bns[5] > 0: 3776 zhl = -min_bns[4] 3777 hzy = shapes.Line([0, 0, zhl], [0, dy, zhl], c=zhighlight_zero_color) 3778 hzy.alpha(np.sqrt(yzalpha)).lw(grid_linewidth * 2) 3779 hzy.shift([yzshift * dx, 0, 0]) 3780 hzy.name = "zyHighlightZero" 3781 highlights.append(hzy) 3782 3783 if zxgrid and ztitle and xtitle: 3784 if zhighlight_zero and min_bns[4] <= 0 and max_bns[5] > 0: 3785 zhl = -min_bns[4] 3786 hzx = shapes.Line([0, 0, zhl], [dx, 0, zhl], c=zhighlight_zero_color) 3787 hzx.alpha(np.sqrt(zxalpha)).lw(grid_linewidth * 2) 3788 hzx.shift([0, zxshift * dy, 0]) 3789 hzx.name = "zxHighlightZero" 3790 highlights.append(hzx) 3791 if xhighlight_zero and min_bns[0] <= 0 and max_bns[1] > 0: 3792 xhl = -min_bns[0] 3793 hxz = shapes.Line([xhl, 0, 0], [xhl, 0, dz], c=xhighlight_zero_color) 3794 hxz.alpha(np.sqrt(zxalpha)).lw(grid_linewidth * 2) 3795 hxz.shift([0, zxshift * dy, 0]) 3796 hxz.name = "xzHighlightZero" 3797 highlights.append(hxz) 3798 3799 ################################################ arrow cone 3800 cones = [] 3801 3802 if tip_size: 3803 3804 if xtitle: 3805 if x_inverted: 3806 cx = shapes.Cone( 3807 r=tip_size, 3808 height=tip_size * 2, 3809 axis=(-1, 0, 0), 3810 c=xline_color, 3811 res=12, 3812 ) 3813 else: 3814 cx = shapes.Cone( 3815 (dx, 0, 0), 3816 r=tip_size, 3817 height=tip_size * 2, 3818 axis=(1, 0, 0), 3819 c=xline_color, 3820 res=12, 3821 ) 3822 T = LinearTransform() 3823 T.translate( 3824 [ 3825 0, 3826 zxshift * dy + xshift_along_y * dy, 3827 xyshift * dz + xshift_along_z * dz, 3828 ] 3829 ) 3830 cx.apply_transform(T) 3831 cx.name = "xTipCone" 3832 cones.append(cx) 3833 3834 if ytitle: 3835 if y_inverted: 3836 cy = shapes.Cone( 3837 r=tip_size, 3838 height=tip_size * 2, 3839 axis=(0, -1, 0), 3840 c=yline_color, 3841 res=12, 3842 ) 3843 else: 3844 cy = shapes.Cone( 3845 (0, dy, 0), 3846 r=tip_size, 3847 height=tip_size * 2, 3848 axis=(0, 1, 0), 3849 c=yline_color, 3850 res=12, 3851 ) 3852 T = LinearTransform() 3853 T.translate( 3854 [ 3855 yzshift * dx + yshift_along_x * dx, 3856 0, 3857 xyshift * dz + yshift_along_z * dz, 3858 ] 3859 ) 3860 cy.apply_transform(T) 3861 cy.name = "yTipCone" 3862 cones.append(cy) 3863 3864 if ztitle: 3865 if z_inverted: 3866 cz = shapes.Cone( 3867 r=tip_size, 3868 height=tip_size * 2, 3869 axis=(0, 0, -1), 3870 c=zline_color, 3871 res=12, 3872 ) 3873 else: 3874 cz = shapes.Cone( 3875 (0, 0, dz), 3876 r=tip_size, 3877 height=tip_size * 2, 3878 axis=(0, 0, 1), 3879 c=zline_color, 3880 res=12, 3881 ) 3882 T = LinearTransform() 3883 T.translate( 3884 [ 3885 yzshift * dx + zshift_along_x * dx, 3886 zxshift * dy + zshift_along_y * dy, 3887 0, 3888 ] 3889 ) 3890 cz.apply_transform(T) 3891 cz.name = "zTipCone" 3892 cones.append(cz) 3893 3894 ################################################################# MAJOR ticks 3895 majorticks, minorticks = [], [] 3896 xticks, yticks, zticks = [], [], [] 3897 if show_ticks: 3898 if xtitle: 3899 tick_thickness = xtick_thickness * gscale / 2 3900 tick_length = xtick_length * gscale / 2 3901 for i in range(1, len(xticks_float) - 1): 3902 v1 = (xticks_float[i] - tick_thickness, -tick_length, 0) 3903 v2 = (xticks_float[i] + tick_thickness, tick_length, 0) 3904 xticks.append(shapes.Rectangle(v1, v2)) 3905 if len(xticks) > 1: 3906 xmajticks = merge(xticks).c(xlabel_color) 3907 T = LinearTransform() 3908 T.rotate_x(xaxis_rotation) 3909 T.translate([0, zxshift*dy + xshift_along_y*dy, xyshift*dz + xshift_along_z*dz]) 3910 xmajticks.apply_transform(T) 3911 xmajticks.name = "xMajorTicks" 3912 majorticks.append(xmajticks) 3913 if ytitle: 3914 tick_thickness = ytick_thickness * gscale / 2 3915 tick_length = ytick_length * gscale / 2 3916 for i in range(1, len(yticks_float) - 1): 3917 v1 = (-tick_length, yticks_float[i] - tick_thickness, 0) 3918 v2 = (tick_length, yticks_float[i] + tick_thickness, 0) 3919 yticks.append(shapes.Rectangle(v1, v2)) 3920 if len(yticks) > 1: 3921 ymajticks = merge(yticks).c(ylabel_color) 3922 T = LinearTransform() 3923 T.rotate_y(yaxis_rotation) 3924 T.translate([yzshift*dx + yshift_along_x*dx, 0, xyshift*dz + yshift_along_z*dz]) 3925 ymajticks.apply_transform(T) 3926 ymajticks.name = "yMajorTicks" 3927 majorticks.append(ymajticks) 3928 if ztitle: 3929 tick_thickness = ztick_thickness * gscale / 2 3930 tick_length = ztick_length * gscale / 2.85 3931 for i in range(1, len(zticks_float) - 1): 3932 v1 = (zticks_float[i] - tick_thickness, -tick_length, 0) 3933 v2 = (zticks_float[i] + tick_thickness, tick_length, 0) 3934 zticks.append(shapes.Rectangle(v1, v2)) 3935 if len(zticks) > 1: 3936 zmajticks = merge(zticks).c(zlabel_color) 3937 T = LinearTransform() 3938 T.rotate_y(-90).rotate_z(-45 + zaxis_rotation) 3939 T.translate([yzshift*dx + zshift_along_x*dx, zxshift*dy + zshift_along_y*dy, 0]) 3940 zmajticks.apply_transform(T) 3941 zmajticks.name = "zMajorTicks" 3942 majorticks.append(zmajticks) 3943 3944 ############################################################# MINOR ticks 3945 if xtitle and xminor_ticks and len(xticks) > 1: 3946 tick_thickness = xtick_thickness * gscale / 4 3947 tick_length = xtick_length * gscale / 4 3948 xminor_ticks += 1 3949 ticks = [] 3950 for i in range(1, len(xticks)): 3951 t0, t1 = xticks[i - 1].pos(), xticks[i].pos() 3952 dt = t1 - t0 3953 for j in range(1, xminor_ticks): 3954 mt = dt * (j / xminor_ticks) + t0 3955 v1 = (mt[0] - tick_thickness, -tick_length, 0) 3956 v2 = (mt[0] + tick_thickness, tick_length, 0) 3957 ticks.append(shapes.Rectangle(v1, v2)) 3958 3959 # finish off the fist lower range from start to first tick 3960 t0, t1 = xticks[0].pos(), xticks[1].pos() 3961 dt = t1 - t0 3962 for j in range(1, xminor_ticks): 3963 mt = t0 - dt * (j / xminor_ticks) 3964 if mt[0] < 0: 3965 break 3966 v1 = (mt[0] - tick_thickness, -tick_length, 0) 3967 v2 = (mt[0] + tick_thickness, tick_length, 0) 3968 ticks.append(shapes.Rectangle(v1, v2)) 3969 3970 # finish off the last upper range from last tick to end 3971 t0, t1 = xticks[-2].pos(), xticks[-1].pos() 3972 dt = t1 - t0 3973 for j in range(1, xminor_ticks): 3974 mt = t1 + dt * (j / xminor_ticks) 3975 if mt[0] > dx: 3976 break 3977 v1 = (mt[0] - tick_thickness, -tick_length, 0) 3978 v2 = (mt[0] + tick_thickness, tick_length, 0) 3979 ticks.append(shapes.Rectangle(v1, v2)) 3980 3981 if ticks: 3982 xminticks = merge(ticks).c(xlabel_color) 3983 T = LinearTransform() 3984 T.rotate_x(xaxis_rotation) 3985 T.translate([0, zxshift*dy + xshift_along_y*dy, xyshift*dz + xshift_along_z*dz]) 3986 xminticks.apply_transform(T) 3987 xminticks.name = "xMinorTicks" 3988 minorticks.append(xminticks) 3989 3990 if ytitle and yminor_ticks and len(yticks) > 1: ##### y 3991 tick_thickness = ytick_thickness * gscale / 4 3992 tick_length = ytick_length * gscale / 4 3993 yminor_ticks += 1 3994 ticks = [] 3995 for i in range(1, len(yticks)): 3996 t0, t1 = yticks[i - 1].pos(), yticks[i].pos() 3997 dt = t1 - t0 3998 for j in range(1, yminor_ticks): 3999 mt = dt * (j / yminor_ticks) + t0 4000 v1 = (-tick_length, mt[1] - tick_thickness, 0) 4001 v2 = (tick_length, mt[1] + tick_thickness, 0) 4002 ticks.append(shapes.Rectangle(v1, v2)) 4003 4004 # finish off the fist lower range from start to first tick 4005 t0, t1 = yticks[0].pos(), yticks[1].pos() 4006 dt = t1 - t0 4007 for j in range(1, yminor_ticks): 4008 mt = t0 - dt * (j / yminor_ticks) 4009 if mt[1] < 0: 4010 break 4011 v1 = (-tick_length, mt[1] - tick_thickness, 0) 4012 v2 = (tick_length, mt[1] + tick_thickness, 0) 4013 ticks.append(shapes.Rectangle(v1, v2)) 4014 4015 # finish off the last upper range from last tick to end 4016 t0, t1 = yticks[-2].pos(), yticks[-1].pos() 4017 dt = t1 - t0 4018 for j in range(1, yminor_ticks): 4019 mt = t1 + dt * (j / yminor_ticks) 4020 if mt[1] > dy: 4021 break 4022 v1 = (-tick_length, mt[1] - tick_thickness, 0) 4023 v2 = (tick_length, mt[1] + tick_thickness, 0) 4024 ticks.append(shapes.Rectangle(v1, v2)) 4025 4026 if ticks: 4027 yminticks = merge(ticks).c(ylabel_color) 4028 T = LinearTransform() 4029 T.rotate_y(yaxis_rotation) 4030 T.translate([yzshift*dx + yshift_along_x*dx, 0, xyshift*dz + yshift_along_z*dz]) 4031 yminticks.apply_transform(T) 4032 yminticks.name = "yMinorTicks" 4033 minorticks.append(yminticks) 4034 4035 if ztitle and zminor_ticks and len(zticks) > 1: ##### z 4036 tick_thickness = ztick_thickness * gscale / 4 4037 tick_length = ztick_length * gscale / 5 4038 zminor_ticks += 1 4039 ticks = [] 4040 for i in range(1, len(zticks)): 4041 t0, t1 = zticks[i - 1].pos(), zticks[i].pos() 4042 dt = t1 - t0 4043 for j in range(1, zminor_ticks): 4044 mt = dt * (j / zminor_ticks) + t0 4045 v1 = (mt[0] - tick_thickness, -tick_length, 0) 4046 v2 = (mt[0] + tick_thickness, tick_length, 0) 4047 ticks.append(shapes.Rectangle(v1, v2)) 4048 4049 # finish off the fist lower range from start to first tick 4050 t0, t1 = zticks[0].pos(), zticks[1].pos() 4051 dt = t1 - t0 4052 for j in range(1, zminor_ticks): 4053 mt = t0 - dt * (j / zminor_ticks) 4054 if mt[0] < 0: 4055 break 4056 v1 = (mt[0] - tick_thickness, -tick_length, 0) 4057 v2 = (mt[0] + tick_thickness, tick_length, 0) 4058 ticks.append(shapes.Rectangle(v1, v2)) 4059 4060 # finish off the last upper range from last tick to end 4061 t0, t1 = zticks[-2].pos(), zticks[-1].pos() 4062 dt = t1 - t0 4063 for j in range(1, zminor_ticks): 4064 mt = t1 + dt * (j / zminor_ticks) 4065 if mt[0] > dz: 4066 break 4067 v1 = (mt[0] - tick_thickness, -tick_length, 0) 4068 v2 = (mt[0] + tick_thickness, tick_length, 0) 4069 ticks.append(shapes.Rectangle(v1, v2)) 4070 4071 if ticks: 4072 zminticks = merge(ticks).c(zlabel_color) 4073 T = LinearTransform() 4074 T.rotate_y(-90).rotate_z(-45 + zaxis_rotation) 4075 T.translate([yzshift*dx + zshift_along_x*dx, zxshift*dy + zshift_along_y*dy, 0]) 4076 zminticks.apply_transform(T) 4077 zminticks.name = "zMinorTicks" 4078 minorticks.append(zminticks) 4079 4080 ################################################ axes NUMERIC text labels 4081 labels = [] 4082 xlab, ylab, zlab = None, None, None 4083 4084 if xlabel_size and xtitle: 4085 4086 xRot, yRot, zRot = 0, 0, 0 4087 if utils.is_sequence(xlabel_rotation): # unpck 3 rotations 4088 zRot, xRot, yRot = xlabel_rotation 4089 else: 4090 zRot = xlabel_rotation 4091 if zRot < 0: # deal with negative angles 4092 zRot += 360 4093 4094 jus = "center-top" 4095 if zRot: 4096 if zRot > 24: jus = "top-right" 4097 if zRot > 67: jus = "center-right" 4098 if zRot > 112: jus = "right-bottom" 4099 if zRot > 157: jus = "center-bottom" 4100 if zRot > 202: jus = "bottom-left" 4101 if zRot > 247: jus = "center-left" 4102 if zRot > 292: jus = "top-left" 4103 if zRot > 337: jus = "top-center" 4104 if xlabel_justify is not None: 4105 jus = xlabel_justify 4106 4107 for i in range(1, len(xticks_str)): 4108 t = xticks_str[i] 4109 if not t: 4110 continue 4111 if utils.is_sequence(xlabel_offset): 4112 xoffs, yoffs, zoffs = xlabel_offset 4113 else: 4114 xoffs, yoffs, zoffs = 0, xlabel_offset, 0 4115 4116 xlab = shapes.Text3D( 4117 t, s=xlabel_size * text_scale * gscale, font=label_font, justify=jus 4118 ) 4119 tb = xlab.ybounds() # must be ybounds: height of char 4120 4121 v = (xticks_float[i], 0, 0) 4122 offs = -np.array([xoffs, yoffs, zoffs]) * (tb[1] - tb[0]) 4123 4124 T = LinearTransform() 4125 T.rotate_x(xaxis_rotation).rotate_y(yRot).rotate_x(xRot).rotate_z(zRot) 4126 T.translate(v + offs) 4127 T.translate([0, zxshift*dy + xshift_along_y*dy, xyshift*dz + xshift_along_z*dz]) 4128 xlab.apply_transform(T) 4129 4130 xlab.use_bounds(x_use_bounds) 4131 4132 xlab.c(xlabel_color) 4133 if xlabel_backface_color is None: 4134 bfc = 1 - np.array(get_color(xlabel_color)) 4135 xlab.backcolor(bfc) 4136 xlab.name = f"xNumericLabel {i}" 4137 labels.append(xlab) 4138 4139 if ylabel_size and ytitle: 4140 4141 xRot, yRot, zRot = 0, 0, 0 4142 if utils.is_sequence(ylabel_rotation): # unpck 3 rotations 4143 zRot, yRot, xRot = ylabel_rotation 4144 else: 4145 zRot = ylabel_rotation 4146 if zRot < 0: 4147 zRot += 360 # deal with negative angles 4148 4149 jus = "center-right" 4150 if zRot: 4151 if zRot > 24: jus = "bottom-right" 4152 if zRot > 67: jus = "center-bottom" 4153 if zRot > 112: jus = "left-bottom" 4154 if zRot > 157: jus = "center-left" 4155 if zRot > 202: jus = "top-left" 4156 if zRot > 247: jus = "center-top" 4157 if zRot > 292: jus = "top-right" 4158 if zRot > 337: jus = "right-center" 4159 if ylabel_justify is not None: 4160 jus = ylabel_justify 4161 4162 for i in range(1, len(yticks_str)): 4163 t = yticks_str[i] 4164 if not t: 4165 continue 4166 if utils.is_sequence(ylabel_offset): 4167 xoffs, yoffs, zoffs = ylabel_offset 4168 else: 4169 xoffs, yoffs, zoffs = ylabel_offset, 0, 0 4170 ylab = shapes.Text3D( 4171 t, s=ylabel_size * text_scale * gscale, font=label_font, justify=jus 4172 ) 4173 tb = ylab.ybounds() # must be ybounds: height of char 4174 v = (0, yticks_float[i], 0) 4175 offs = -np.array([xoffs, yoffs, zoffs]) * (tb[1] - tb[0]) 4176 4177 T = LinearTransform() 4178 T.rotate_y(yaxis_rotation).rotate_x(xRot).rotate_y(yRot).rotate_z(zRot) 4179 T.translate(v + offs) 4180 T.translate([yzshift*dx + yshift_along_x*dx, 0, xyshift*dz + yshift_along_z*dz]) 4181 ylab.apply_transform(T) 4182 4183 ylab.use_bounds(y_use_bounds) 4184 4185 ylab.c(ylabel_color) 4186 if ylabel_backface_color is None: 4187 bfc = 1 - np.array(get_color(ylabel_color)) 4188 ylab.backcolor(bfc) 4189 ylab.name = f"yNumericLabel {i}" 4190 labels.append(ylab) 4191 4192 if zlabel_size and ztitle: 4193 4194 xRot, yRot, zRot = 0, 0, 0 4195 if utils.is_sequence(zlabel_rotation): # unpck 3 rotations 4196 xRot, yRot, zRot = zlabel_rotation 4197 else: 4198 xRot = zlabel_rotation 4199 if xRot < 0: xRot += 360 # deal with negative angles 4200 4201 jus = "center-right" 4202 if xRot: 4203 if xRot > 24: jus = "bottom-right" 4204 if xRot > 67: jus = "center-bottom" 4205 if xRot > 112: jus = "left-bottom" 4206 if xRot > 157: jus = "center-left" 4207 if xRot > 202: jus = "top-left" 4208 if xRot > 247: jus = "center-top" 4209 if xRot > 292: jus = "top-right" 4210 if xRot > 337: jus = "right-center" 4211 if zlabel_justify is not None: 4212 jus = zlabel_justify 4213 4214 for i in range(1, len(zticks_str)): 4215 t = zticks_str[i] 4216 if not t: 4217 continue 4218 if utils.is_sequence(zlabel_offset): 4219 xoffs, yoffs, zoffs = zlabel_offset 4220 else: 4221 xoffs, yoffs, zoffs = zlabel_offset, zlabel_offset, 0 4222 zlab = shapes.Text3D(t, s=zlabel_size*text_scale*gscale, font=label_font, justify=jus) 4223 tb = zlab.ybounds() # must be ybounds: height of char 4224 4225 v = (0, 0, zticks_float[i]) 4226 offs = -np.array([xoffs, yoffs, zoffs]) * (tb[1] - tb[0]) / 1.5 4227 angle = np.arctan2(dy, dx) * 57.3 4228 4229 T = LinearTransform() 4230 T.rotate_x(90 + zRot).rotate_y(-xRot).rotate_z(angle + yRot + zaxis_rotation) 4231 T.translate(v + offs) 4232 T.translate([yzshift*dx + zshift_along_x*dx, zxshift*dy + zshift_along_y*dy, 0]) 4233 zlab.apply_transform(T) 4234 4235 zlab.use_bounds(z_use_bounds) 4236 4237 zlab.c(zlabel_color) 4238 if zlabel_backface_color is None: 4239 bfc = 1 - np.array(get_color(zlabel_color)) 4240 zlab.backcolor(bfc) 4241 zlab.name = f"zNumericLabel {i}" 4242 labels.append(zlab) 4243 4244 ################################################ axes titles 4245 titles = [] 4246 4247 if xtitle: 4248 xRot, yRot, zRot = 0, 0, 0 4249 if utils.is_sequence(xtitle_rotation): # unpack 3 rotations 4250 zRot, xRot, yRot = xtitle_rotation 4251 else: 4252 zRot = xtitle_rotation 4253 if zRot < 0: # deal with negative angles 4254 zRot += 360 4255 4256 if utils.is_sequence(xtitle_offset): 4257 xoffs, yoffs, zoffs = xtitle_offset 4258 else: 4259 xoffs, yoffs, zoffs = 0, xtitle_offset, 0 4260 4261 if xtitle_justify is not None: 4262 jus = xtitle_justify 4263 else: 4264 # find best justfication for given rotation(s) 4265 jus = "right-top" 4266 if zRot: 4267 if zRot > 24: jus = "center-right" 4268 if zRot > 67: jus = "right-bottom" 4269 if zRot > 157: jus = "bottom-left" 4270 if zRot > 202: jus = "center-left" 4271 if zRot > 247: jus = "top-left" 4272 if zRot > 337: jus = "top-right" 4273 4274 xt = shapes.Text3D( 4275 xtitle, 4276 s=xtitle_size * text_scale * gscale, 4277 font=title_font, 4278 c=xtitle_color, 4279 justify=jus, 4280 depth=title_depth, 4281 italic=xtitle_italic, 4282 ) 4283 if xtitle_backface_color is None: 4284 xtitle_backface_color = 1 - np.array(get_color(xtitle_color)) 4285 xt.backcolor(xtitle_backface_color) 4286 4287 shift = 0 4288 if xlab: # xlab is the last created numeric text label.. 4289 lt0, lt1 = xlab.bounds()[2:4] 4290 shift = lt1 - lt0 4291 4292 T = LinearTransform() 4293 T.rotate_x(xRot).rotate_y(yRot).rotate_z(zRot) 4294 T.set_position( 4295 [(xoffs + xtitle_position) * dx, 4296 -(yoffs + xtick_length / 2) * dy - shift, 4297 zoffs * dz] 4298 ) 4299 T.rotate_x(xaxis_rotation) 4300 T.translate([0, xshift_along_y * dy, xyshift * dz + xshift_along_z * dz]) 4301 xt.apply_transform(T) 4302 4303 xt.use_bounds(x_use_bounds) 4304 if xtitle == " ": 4305 xt.use_bounds(False) 4306 xt.name = "xtitle" 4307 titles.append(xt) 4308 if xtitle_box: 4309 titles.append(xt.box(scale=1.1).use_bounds(x_use_bounds)) 4310 4311 if ytitle: 4312 xRot, yRot, zRot = 0, 0, 0 4313 if utils.is_sequence(ytitle_rotation): # unpck 3 rotations 4314 zRot, yRot, xRot = ytitle_rotation 4315 else: 4316 zRot = ytitle_rotation 4317 if