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 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  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  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  624 625 - [timer_callback2.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/timer_callback2.py) 626 627  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  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 self.previous_value = None 969 970 @property 971 def interactor(self): 972 return self.GetInteractor() 973 974 @interactor.setter 975 def interactor(self, iren): 976 self.SetInteractor(iren) 977 978 @property 979 def representation(self): 980 return self.GetRepresentation() 981 982 @property 983 def value(self): 984 val = self.GetRepresentation().GetValue() 985 # self.previous_value = val 986 return val 987 988 @value.setter 989 def value(self, val): 990 self.GetRepresentation().SetValue(val) 991 992 @property 993 def renderer(self): 994 return self.GetCurrentRenderer() 995 996 @renderer.setter 997 def renderer(self, ren): 998 self.SetCurrentRenderer(ren) 999 1000 @property 1001 def title(self): 1002 self.GetRepresentation().GetTitleText() 1003 1004 @title.setter 1005 def title(self, txt): 1006 self.GetRepresentation().SetTitleText(str(txt)) 1007 1008 @property 1009 def range(self): 1010 xmin = self.GetRepresentation().GetMinimumValue() 1011 xmax = self.GetRepresentation().GetMaximumValue() 1012 return [xmin, xmax] 1013 1014 @range.setter 1015 def range(self, vals): 1016 if vals[0] is not None: 1017 self.GetRepresentation().SetMinimumValue(vals[0]) 1018 if vals[1] is not None: 1019 self.GetRepresentation().SetMaximumValue(vals[1]) 1020 1021 def on(self) -> Self: 1022 self.EnabledOn() 1023 return self 1024 1025 def off(self) -> Self: 1026 self.EnabledOff() 1027 return self 1028 1029 def toggle(self) -> Self: 1030 self.SetEnabled(not self.GetEnabled()) 1031 return self 1032 1033 def add_observer(self, event, func, priority=1) -> int: 1034 """Add an observer to the widget.""" 1035 event = utils.get_vtk_name_event(event) 1036 cid = self.AddObserver(event, func, priority) 1037 return cid 1038 1039 1040##################################################################### 1041def Goniometer( 1042 p1, 1043 p2, 1044 p3, 1045 font="", 1046 arc_size=0.4, 1047 s=1, 1048 italic=0, 1049 rotation=0, 1050 prefix="", 1051 lc="k2", 1052 c="white", 1053 alpha=1, 1054 lw=2, 1055 precision=3, 1056): 1057 """ 1058 Build a graphical goniometer to measure the angle formed by 3 points in space. 1059 1060 Arguments: 1061 p1 : (list) 1062 first point 3D coordinates. 1063 p2 : (list) 1064 the vertex point. 1065 p3 : (list) 1066 the last point defining the angle. 1067 font : (str) 1068 Font face. Check [available fonts here](https://vedo.embl.es/fonts). 1069 arc_size : (float) 1070 dimension of the arc wrt the smallest axis. 1071 s : (float) 1072 size of the text. 1073 italic : (float, bool) 1074 italic text. 1075 rotation : (float) 1076 rotation of text in degrees. 1077 prefix : (str) 1078 append this string to the numeric value of the angle. 1079 lc : (list) 1080 color of the goniometer lines. 1081 c : (str) 1082 color of the goniometer angle filling. Set alpha=0 to remove it. 1083 alpha : (float) 1084 transparency level. 1085 lw : (float) 1086 line width. 1087 precision : (int) 1088 number of significant digits. 1089 1090 Examples: 1091 - [goniometer.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/goniometer.py) 1092 1093  1094 """ 1095 if isinstance(p1, Points): p1 = p1.pos() 1096 if isinstance(p2, Points): p2 = p2.pos() 1097 if isinstance(p3, Points): p3 = p3.pos() 1098 if len(p1)==2: p1=[p1[0], p1[1], 0.0] 1099 if len(p2)==2: p2=[p2[0], p2[1], 0.0] 1100 if len(p3)==2: p3=[p3[0], p3[1], 0.0] 1101 p1, p2, p3 = np.array(p1), np.array(p2), np.array(p3) 1102 1103 acts = [] 1104 ln = shapes.Line([p1, p2, p3], lw=lw, c=lc) 1105 acts.append(ln) 1106 1107 va = utils.versor(p1 - p2) 1108 vb = utils.versor(p3 - p2) 1109 r = min(utils.mag(p3 - p2), utils.mag(p1 - p2)) * arc_size 1110 ptsarc = [] 1111 res = 120 1112 imed = int(res / 2) 1113 for i in range(res + 1): 1114 vi = utils.versor(vb * i / res + va * (res - i) / res) 1115 if i == imed: 1116 vc = np.array(vi) 1117 ptsarc.append(p2 + vi * r) 1118 arc = shapes.Line(ptsarc).lw(lw).c(lc) 1119 acts.append(arc) 1120 1121 angle = np.arccos(np.dot(va, vb)) * 180 / np.pi 1122 1123 lb = shapes.Text3D( 1124 prefix + utils.precision(angle, precision) + "º", 1125 s=r / 12 * s, 1126 font=font, 1127 italic=italic, 1128 justify="center", 1129 ) 1130 cr = np.cross(va, vb) 1131 lb.reorient([0, 0, 1], cr * np.sign(cr[2]), rotation=rotation, xyplane=False) 1132 lb.pos(p2 + vc * r / 1.75) 1133 lb.c(c).bc("tomato").lighting("off") 1134 acts.append(lb) 1135 1136 if alpha > 0: 1137 pts = [p2] + arc.coordinates.tolist() + [p2] 1138 msh = Mesh([pts, [list(range(arc.npoints + 2))]], c=lc, alpha=alpha) 1139 msh.lighting("off") 1140 msh.triangulate() 1141 msh.shift(0, 0, -r / 10000) # to resolve 2d conflicts.. 1142 acts.append(msh) 1143 1144 asse = Assembly(acts) 1145 asse.name = "Goniometer" 1146 return asse 1147 1148 1149def Light(pos, focal_point=(0, 0, 0), angle=180, c=None, intensity=1): 1150 """ 1151 Generate a source of light placed at `pos` and directed to `focal point`. 1152 Returns a `vtkLight` object. 1153 1154 Arguments: 1155 focal_point : (list) 1156 focal point, if a `vedo` object is passed then will grab its position. 1157 angle : (float) 1158 aperture angle of the light source, in degrees 1159 c : (color) 1160 set the light color 1161 intensity : (float) 1162 intensity value between 0 and 1. 1163 1164 Check also: 1165 `plotter.Plotter.remove_lights()` 1166 1167 Examples: 1168 - [light_sources.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/light_sources.py) 1169 1170  1171 """ 1172 if c is None: 1173 try: 1174 c = pos.color() 1175 except AttributeError: 1176 c = "white" 1177 1178 try: 1179 pos = pos.pos() 1180 except AttributeError: 1181 pass 1182 1183 try: 1184 focal_point = focal_point.pos() 1185 except AttributeError: 1186 pass 1187 1188 light = vtki.vtkLight() 1189 light.SetLightTypeToSceneLight() 1190 light.SetPosition(pos) 1191 light.SetConeAngle(angle) 1192 light.SetFocalPoint(focal_point) 1193 light.SetIntensity(intensity) 1194 light.SetColor(get_color(c)) 1195 return light 1196 1197 1198##################################################################### 1199def ScalarBar( 1200 obj, 1201 title="", 1202 pos=(), 1203 size=(80, 400), 1204 font_size=14, 1205 title_yoffset=20, 1206 nlabels=None, 1207 c="k", 1208 horizontal=False, 1209 use_alpha=True, 1210 label_format=":6.3g", 1211) -> Union[vtki.vtkScalarBarActor, None]: 1212 """ 1213 A 2D scalar bar for the specified object. 1214 1215 Arguments: 1216 title : (str) 1217 scalar bar title 1218 pos : (list) 1219 position coordinates of the bottom left corner. 1220 Can also be a pair of (x,y) values in the range [0,1] 1221 to indicate the position of the bottom-left and top-right corners. 1222 size : (float,float) 1223 size of the scalarbar in number of pixels (width, height) 1224 font_size : (float) 1225 size of font for title and numeric labels 1226 title_yoffset : (float) 1227 vertical space offset between title and color scalarbar 1228 nlabels : (int) 1229 number of numeric labels 1230 c : (list) 1231 color of the scalar bar text 1232 horizontal : (bool) 1233 lay the scalarbar horizontally 1234 use_alpha : (bool) 1235 render transparency in the color bar itself 1236 label_format : (str) 1237 c-style format string for numeric labels 1238 1239 Examples: 1240 - [scalarbars.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/scalarbars.py) 1241 1242  1243 """ 1244 1245 if isinstance(obj, (Points, TetMesh, vedo.UnstructuredGrid)): 1246 vtkscalars = obj.dataset.GetPointData().GetScalars() 1247 if vtkscalars is None: 1248 vtkscalars = obj.dataset.GetCellData().GetScalars() 1249 if not vtkscalars: 1250 return None 1251 lut = vtkscalars.GetLookupTable() 1252 if not lut: 1253 lut = obj.mapper.GetLookupTable() 1254 if not lut: 1255 return None 1256 1257 elif isinstance(obj, Volume): 1258 lut = utils.ctf2lut(obj) 1259 1260 elif utils.is_sequence(obj) and len(obj) == 2: 1261 x = np.linspace(obj[0], obj[1], 256) 1262 data = [] 1263 for i in range(256): 1264 rgb = color_map(i, c, 0, 256) 1265 data.append([x[i], rgb]) 1266 lut = build_lut(data) 1267 1268 elif not hasattr(obj, "mapper"): 1269 vedo.logger.error(f"in add_scalarbar(): input is invalid {type(obj)}. Skip.") 1270 return None 1271 1272 else: 1273 return None 1274 1275 c = get_color(c) 1276 sb = vtki.vtkScalarBarActor() 1277 1278 # print("GetLabelFormat", sb.GetLabelFormat()) 1279 label_format = label_format.replace(":", "%-#") 1280 sb.SetLabelFormat(label_format) 1281 1282 sb.SetLookupTable(lut) 1283 sb.SetUseOpacity(use_alpha) 1284 sb.SetDrawFrame(0) 1285 sb.SetDrawBackground(0) 1286 if lut.GetUseBelowRangeColor(): 1287 sb.DrawBelowRangeSwatchOn() 1288 sb.SetBelowRangeAnnotation("") 1289 if lut.GetUseAboveRangeColor(): 1290 sb.DrawAboveRangeSwatchOn() 1291 sb.SetAboveRangeAnnotation("") 1292 if lut.GetNanColor() != (0.5, 0.0, 0.0, 1.0): 1293 sb.DrawNanAnnotationOn() 1294 sb.SetNanAnnotation("nan") 1295 1296 if title: 1297 if "\\" in repr(title): 1298 for r in shapes._reps: 1299 title = title.replace(r[0], r[1]) 1300 titprop = sb.GetTitleTextProperty() 1301 titprop.BoldOn() 1302 titprop.ItalicOff() 1303 titprop.ShadowOff() 1304 titprop.SetColor(c) 1305 titprop.SetVerticalJustificationToTop() 1306 titprop.SetFontSize(font_size) 1307 titprop.SetFontFamily(vtki.VTK_FONT_FILE) 1308 titprop.SetFontFile(utils.get_font_path(vedo.settings.default_font)) 1309 sb.SetTitle(title) 1310 sb.SetVerticalTitleSeparation(title_yoffset) 1311 sb.SetTitleTextProperty(titprop) 1312 1313 sb.SetTextPad(0) 1314 sb.UnconstrainedFontSizeOn() 1315 sb.DrawAnnotationsOn() 1316 sb.DrawTickLabelsOn() 1317 sb.SetMaximumNumberOfColors(256) 1318 if nlabels is not None: 1319 sb.SetNumberOfLabels(nlabels) 1320 1321 if len(pos) == 0 or utils.is_sequence(pos[0]): 1322 if len(pos) == 0: 1323 pos = ((0.87, 0.05), (0.97, 0.5)) 1324 if horizontal: 1325 pos = ((0.5, 0.05), (0.97, 0.15)) 1326 sb.SetTextPositionToPrecedeScalarBar() 1327 if horizontal: 1328 if not nlabels: sb.SetNumberOfLabels(3) 1329 sb.SetOrientationToHorizontal() 1330 sb.SetTextPositionToSucceedScalarBar() 1331 sb.GetPositionCoordinate().SetCoordinateSystemToNormalizedViewport() 1332 sb.GetPosition2Coordinate().SetCoordinateSystemToNormalizedViewport() 1333 1334 s = np.array(pos[1]) - np.array(pos[0]) 1335 sb.GetPositionCoordinate().SetValue(pos[0][0], pos[0][1]) 1336 sb.GetPosition2Coordinate().SetValue(s[0], s[1]) # size !!?? 1337 1338 else: 1339 1340 if horizontal: 1341 size = (size[1], size[0]) # swap size 1342 sb.SetPosition(pos[0]-0.7, pos[1]) 1343 if not nlabels: sb.SetNumberOfLabels(3) 1344 sb.SetOrientationToHorizontal() 1345 sb.SetTextPositionToSucceedScalarBar() 1346 else: 1347 sb.SetPosition(pos[0], pos[1]) 1348 if not nlabels: sb.SetNumberOfLabels(7) 1349 sb.SetTextPositionToPrecedeScalarBar() 1350 sb.SetHeight(1) 1351 sb.SetWidth(1) 1352 if size[0] is not None: sb.SetMaximumWidthInPixels(size[0]) 1353 if size[1] is not None: sb.SetMaximumHeightInPixels(size[1]) 1354 1355 sctxt = sb.GetLabelTextProperty() 1356 sctxt.SetFontFamily(vtki.VTK_FONT_FILE) 1357 sctxt.SetFontFile(utils.get_font_path(vedo.settings.default_font)) 1358 sctxt.SetColor(c) 1359 sctxt.SetShadow(0) 1360 sctxt.SetFontSize(font_size) 1361 sb.SetAnnotationTextProperty(sctxt) 1362 sb.PickableOff() 1363 return sb 1364 1365 1366##################################################################### 1367def ScalarBar3D( 1368 obj, 1369 title="", 1370 pos=None, 1371 size=(0, 0), 1372 title_font="", 1373 title_xoffset=-1.2, 1374 title_yoffset=0.0, 1375 title_size=1.5, 1376 title_rotation=0.0, 1377 nlabels=8, 1378 label_font="", 1379 label_size=1, 1380 label_offset=0.375, 1381 label_rotation=0, 1382 label_format="", 1383 italic=0, 1384 c="k", 1385 draw_box=True, 1386 above_text=None, 1387 below_text=None, 1388 nan_text="NaN", 1389 categories=None, 1390) -> Union[Assembly, None]: 1391 """ 1392 Create a 3D scalar bar for the specified object. 1393 1394 Input `obj` input can be: 1395 1396 - a look-up-table, 1397 - a Mesh already containing a set of scalars associated to vertices or cells, 1398 - if None the last object in the list of actors will be used. 1399 1400 Arguments: 1401 size : (list) 1402 (thickness, length) of scalarbar 1403 title : (str) 1404 scalar bar title 1405 title_xoffset : (float) 1406 horizontal space btw title and color scalarbar 1407 title_yoffset : (float) 1408 vertical space offset 1409 title_size : (float) 1410 size of title wrt numeric labels 1411 title_rotation : (float) 1412 title rotation in degrees 1413 nlabels : (int) 1414 number of numeric labels 1415 label_font : (str) 1416 font type for labels 1417 label_size : (float) 1418 label scale factor 1419 label_offset : (float) 1420 space btw numeric labels and scale 1421 label_rotation : (float) 1422 label rotation in degrees 1423 draw_box : (bool) 1424 draw a box around the colorbar 1425 categories : (list) 1426 make a categorical scalarbar, 1427 the input list will have the format [value, color, alpha, textlabel] 1428 1429 Examples: 1430 - [scalarbars.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/scalarbars.py) 1431 - [plot_fxy2.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/plot_fxy2.py) 1432 """ 1433 1434 if isinstance(obj, (Points, TetMesh, vedo.UnstructuredGrid)): 1435 lut = obj.mapper.GetLookupTable() 1436 if not lut or lut.GetTable().GetNumberOfTuples() == 0: 1437 # create the most similar to the default 1438 obj.cmap("jet_r") 1439 lut = obj.mapper.GetLookupTable() 1440 vmin, vmax = lut.GetRange() 1441 1442 elif isinstance(obj, Volume): 1443 lut = utils.ctf2lut(obj) 1444 vmin, vmax = lut.GetRange() 1445 1446 elif isinstance(obj, vtki.vtkLookupTable): 1447 lut = obj 1448 vmin, vmax = lut.GetRange() 1449 1450 else: 1451 vedo.logger.error("in ScalarBar3D(): input must be a vedo object with bounds.") 1452 return None 1453 1454 bns = obj.bounds() 1455 sx, sy = size 1456 if sy == 0 or sy is None: 1457 sy = bns[3] - bns[2] 1458 if sx == 0 or sx is None: 1459 sx = sy / 18 1460 1461 if categories is not None: ################################ 1462 ncats = len(categories) 1463 scale = shapes.Grid([-float(sx) * label_offset, 0, 0], 1464 c=c, alpha=1, s=(sx, sy), res=(1, ncats)) 1465 cols, alphas = [], [] 1466 ticks_pos, ticks_txt = [0.0], [""] 1467 for i, cat in enumerate(categories): 1468 cl = get_color(cat[1]) 1469 cols.append(cl) 1470 if len(cat) > 2: 1471 alphas.append(cat[2]) 1472 else: 1473 alphas.append(1) 1474 if len(cat) > 3: 1475 ticks_txt.append(cat[3]) 1476 else: 1477 ticks_txt.append("") 1478 ticks_pos.append((i + 0.5) / ncats) 1479 ticks_pos.append(1.0) 1480 ticks_txt.append("") 1481 rgba = np.c_[np.array(cols) * 255, np.array(alphas) * 255] 1482 scale.cellcolors = rgba 1483 1484 else: ######################################################## 1485 1486 # build the color scale part 1487 scale = shapes.Grid( 1488 [-float(sx) * label_offset, 0, 0], 1489 c=c, 1490 s=(sx, sy), 1491 res=(1, lut.GetTable().GetNumberOfTuples()), 1492 ) 1493 cscals = np.linspace(vmin, vmax, lut.GetTable().GetNumberOfTuples(), endpoint=True) 1494 1495 if lut.GetScale(): # logarithmic scale 1496 lut10 = vtki.vtkLookupTable() 1497 lut10.DeepCopy(lut) 1498 lut10.SetScaleToLinear() 1499 lut10.Build() 1500 scale.cmap(lut10, cscals, on="cells") 1501 tk = utils.make_ticks(vmin, vmax, nlabels, logscale=True, useformat=label_format) 1502 else: 1503 # for i in range(lut.GetTable().GetNumberOfTuples()): 1504 # print("LUT i=", i, lut.GetTableValue(i)) 1505 scale.cmap(lut, cscals, on="cells") 1506 tk = utils.make_ticks(vmin, vmax, nlabels, logscale=False, useformat=label_format) 1507 ticks_pos, ticks_txt = tk 1508 1509 scale.lw(0).wireframe(False).lighting("off") 1510 1511 scales = [scale] 1512 1513 xbns = scale.xbounds() 1514 1515 lsize = sy / 60 * label_size 1516 1517 tacts = [] 1518 for i, p in enumerate(ticks_pos): 1519 tx = ticks_txt[i] 1520 if i and tx: 1521 # build numeric text 1522 y = (p - 0.5) * sy 1523 if label_rotation: 1524 a = shapes.Text3D( 1525 tx, 1526 s=lsize, 1527 justify="center-top", 1528 c=c, 1529 italic=italic, 1530 font=label_font, 1531 ) 1532 a.rotate_z(label_rotation) 1533 a.pos(sx * label_offset, y, 0) 1534 else: 1535 a = shapes.Text3D( 1536 tx, 1537 pos=[sx * label_offset, y, 0], 1538 s=lsize, 1539 justify="center-left", 1540 c=c, 1541 italic=italic, 1542 font=label_font, 1543 ) 1544 1545 tacts.append(a) 1546 1547 # build ticks 1548 tic = shapes.Line([xbns[1], y, 0], [xbns[1] + sx * label_offset / 4, y, 0], lw=2, c=c) 1549 tacts.append(tic) 1550 1551 # build title 1552 if title: 1553 t = shapes.Text3D( 1554 title, 1555 pos=(0, 0, 0), 1556 s=sy / 50 * title_size, 1557 c=c, 1558 justify="centered-bottom", 1559 italic=italic, 1560 font=title_font, 1561 ) 1562 t.rotate_z(90 + title_rotation) 1563 t.pos(sx * title_xoffset, title_yoffset, 0) 1564 tacts.append(t) 1565 1566 if pos is None: 1567 tsize = 0 1568 if title: 1569 bbt = t.bounds() 1570 tsize = bbt[1] - bbt[0] 1571 pos = (bns[1] + tsize + sx * 1.5, (bns[2] + bns[3]) / 2, bns[4]) 1572 1573 # build below scale 1574 if lut.GetUseBelowRangeColor(): 1575 r, g, b, alfa = lut.GetBelowRangeColor() 1576 sx = float(sx) 1577 sy = float(sy) 1578 brect = shapes.Rectangle( 1579 [-sx * label_offset - sx / 2, -sy / 2 - sx - sx * 0.1, 0], 1580 [-sx * label_offset + sx / 2, -sy / 2 - sx * 0.1, 0], 1581 c=(r, g, b), 1582 alpha=alfa, 1583 ) 1584 brect.lw(1).lc(c).lighting("off") 1585 scales += [brect] 1586 if below_text is None: 1587 below_text = " <" + str(vmin) 1588 if below_text: 1589 if label_rotation: 1590 btx = shapes.Text3D( 1591 below_text, 1592 pos=(0, 0, 0), 1593 s=lsize, 1594 c=c, 1595 justify="center-top", 1596 italic=italic, 1597 font=label_font, 1598 ) 1599 btx.rotate_z(label_rotation) 1600 else: 1601 btx = shapes.Text3D( 1602 below_text, 1603 pos=(0, 0, 0), 1604 s=lsize, 1605 c=c, 1606 justify="center-left", 1607 italic=italic, 1608 font=label_font, 1609 ) 1610 1611 btx.pos(sx * label_offset, -sy / 2 - sx * 0.66, 0) 1612 tacts.append(btx) 1613 1614 # build above scale 1615 if lut.GetUseAboveRangeColor(): 1616 r, g, b, alfa = lut.GetAboveRangeColor() 1617 arect = shapes.Rectangle( 1618 [-sx * label_offset - sx / 2, sy / 2 + sx * 0.1, 0], 1619 [-sx * label_offset + sx / 2, sy / 2 + sx + sx * 0.1, 0], 1620 c=(r, g, b), 1621 alpha=alfa, 1622 ) 1623 arect.lw(1).lc(c).lighting("off") 1624 scales += [arect] 1625 if above_text is None: 1626 above_text = " >" + str(vmax) 1627 if above_text: 1628 if label_rotation: 1629 atx = shapes.Text3D( 1630 above_text, 1631 pos=(0, 0, 0), 1632 s=lsize, 1633 c=c, 1634 justify="center-top", 1635 italic=italic, 1636 font=label_font, 1637 ) 1638 atx.rotate_z(label_rotation) 1639 else: 1640 atx = shapes.Text3D( 1641 above_text, 1642 pos=(0, 0, 0), 1643 s=lsize, 1644 c=c, 1645 justify="center-left", 1646 italic=italic, 1647 font=label_font, 1648 ) 1649 1650 atx.pos(sx * label_offset, sy / 2 + sx * 0.66, 0) 1651 tacts.append(atx) 1652 1653 # build NaN scale 1654 if lut.GetNanColor() != (0.5, 0.0, 0.0, 1.0): 1655 nanshift = sx * 0.1 1656 if brect: 1657 nanshift += sx 1658 r, g, b, alfa = lut.GetNanColor() 1659 nanrect = shapes.Rectangle( 1660 [-sx * label_offset - sx / 2, -sy / 2 - sx - sx * 0.1 - nanshift, 0], 1661 [-sx * label_offset + sx / 2, -sy / 2 - sx * 0.1 - nanshift, 0], 1662 c=(r, g, b), 1663 alpha=alfa, 1664 ) 1665 nanrect.lw(1).lc(c).lighting("off") 1666 scales += [nanrect] 1667 if label_rotation: 1668 nantx = shapes.Text3D( 1669 nan_text, 1670 pos=(0, 0, 0), 1671 s=lsize, 1672 c=c, 1673 justify="center-left", 1674 italic=italic, 1675 font=label_font, 1676 ) 1677 nantx.rotate_z(label_rotation) 1678 else: 1679 nantx = shapes.Text3D( 1680 nan_text, 1681 pos=(0, 0, 0), 1682 s=lsize, 1683 c=c, 1684 justify="center-left", 1685 italic=italic, 1686 font=label_font, 1687 ) 1688 nantx.pos(sx * label_offset, -sy / 2 - sx * 0.66 - nanshift, 0) 1689 tacts.append(nantx) 1690 1691 if draw_box: 1692 tacts.append(scale.box().lw(1).c(c)) 1693 1694 for m in tacts + scales: 1695 m.shift(pos) 1696 m.actor.PickableOff() 1697 m.properties.LightingOff() 1698 1699 asse = Assembly(scales + tacts) 1700 1701 # asse.transform = LinearTransform().shift(pos) 1702 1703 bb = asse.GetBounds() 1704 # print("ScalarBar3D pos",pos, bb) 1705 # asse.SetOrigin(pos) 1706 1707 asse.SetOrigin(bb[0], bb[2], bb[4]) 1708 # asse.SetOrigin(bb[0],0,0) #in pyplot line 1312 1709 1710 asse.PickableOff() 1711 asse.UseBoundsOff() 1712 asse.name = "ScalarBar3D" 1713 return asse 1714 1715 1716##################################################################### 1717class Slider2D(SliderWidget): 1718 """ 1719 Add a slider which can call an external custom function. 1720 """ 1721 1722 def __init__( 1723 self, 1724 sliderfunc, 1725 xmin, 1726 xmax, 1727 value=None, 1728 pos=4, 1729 title="", 1730 font="Calco", 1731 title_size=1, 1732 c="k", 1733 alpha=1, 1734 show_value=True, 1735 delayed=False, 1736 **options, 1737 ): 1738 """ 1739 Add a slider which can call an external custom function. 1740 Set any value as float to increase the number of significant digits above the slider. 1741 1742 Use `play()` to start an animation between the current slider value and the last value. 1743 1744 Arguments: 1745 sliderfunc : (function) 1746 external function to be called by the widget 1747 xmin : (float) 1748 lower value of the slider 1749 xmax : (float) 1750 upper value 1751 value : (float) 1752 current value 1753 pos : (list, str) 1754 position corner number: horizontal [1-5] or vertical [11-15] 1755 it can also be specified by corners coordinates [(x1,y1), (x2,y2)] 1756 and also by a string descriptor (eg. "bottom-left") 1757 title : (str) 1758 title text 1759 font : (str) 1760 title font face. Check [available fonts here](https://vedo.embl.es/fonts). 1761 title_size : (float) 1762 title text scale [1.0] 1763 show_value : (bool) 1764 if True current value is shown 1765 delayed : (bool) 1766 if True the callback is delayed until when the mouse button is released 1767 alpha : (float) 1768 opacity of the scalar bar texts 1769 slider_length : (float) 1770 slider length 1771 slider_width : (float) 1772 slider width 1773 end_cap_length : (float) 1774 length of the end cap 1775 end_cap_width : (float) 1776 width of the end cap 1777 tube_width : (float) 1778 width of the tube 1779 title_height : (float) 1780 height of the title 1781 tformat : (str) 1782 format of the title 1783 1784 Examples: 1785 - [sliders1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/sliders1.py) 1786 - [sliders2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/sliders2.py) 1787 1788  1789 """ 1790 slider_length = options.pop("slider_length", 0.015) 1791 slider_width = options.pop("slider_width", 0.025) 1792 end_cap_length= options.pop("end_cap_length", 0.0015) 1793 end_cap_width = options.pop("end_cap_width", 0.0125) 1794 tube_width = options.pop("tube_width", 0.0075) 1795 title_height = options.pop("title_height", 0.025) 1796 tformat = options.pop("tformat", None) 1797 1798 if options: 1799 vedo.logger.warning(f"in Slider2D unknown option(s): {options}") 1800 1801 c = get_color(c) 1802 1803 if value is None or value < xmin: 1804 value = xmin 1805 1806 slider_rep = vtki.new("SliderRepresentation2D") 1807 slider_rep.SetMinimumValue(xmin) 1808 slider_rep.SetMaximumValue(xmax) 1809 slider_rep.SetValue(value) 1810 slider_rep.SetSliderLength(slider_length) 1811 slider_rep.SetSliderWidth(slider_width) 1812 slider_rep.SetEndCapLength(end_cap_length) 1813 slider_rep.SetEndCapWidth(end_cap_width) 1814 slider_rep.SetTubeWidth(tube_width) 1815 slider_rep.GetPoint1Coordinate().SetCoordinateSystemToNormalizedDisplay() 1816 slider_rep.GetPoint2Coordinate().SetCoordinateSystemToNormalizedDisplay() 1817 1818 if isinstance(pos, str): 1819 if "top" in pos: 1820 if "left" in pos: 1821 if "vert" in pos: 1822 pos = 11 1823 else: 1824 pos = 1 1825 elif "right" in pos: 1826 if "vert" in pos: 1827 pos = 12 1828 else: 1829 pos = 2 1830 elif "bott" in pos: 1831 if "left" in pos: 1832 if "vert" in pos: 1833 pos = 13 1834 else: 1835 pos = 3 1836 elif "right" in pos: 1837 if "vert" in pos: 1838 if "span" in pos: 1839 pos = 15 1840 else: 1841 pos = 14 1842 else: 1843 pos = 4 1844 elif "span" in pos: 1845 pos = 5 1846 1847 if utils.is_sequence(pos): 1848 slider_rep.GetPoint1Coordinate().SetValue(pos[0][0], pos[0][1]) 1849 slider_rep.GetPoint2Coordinate().SetValue(pos[1][0], pos[1][1]) 1850 elif pos == 1: # top-left horizontal 1851 slider_rep.GetPoint1Coordinate().SetValue(0.04, 0.93) 1852 slider_rep.GetPoint2Coordinate().SetValue(0.45, 0.93) 1853 elif pos == 2: 1854 slider_rep.GetPoint1Coordinate().SetValue(0.55, 0.93) 1855 slider_rep.GetPoint2Coordinate().SetValue(0.95, 0.93) 1856 elif pos == 3: 1857 slider_rep.GetPoint1Coordinate().SetValue(0.05, 0.06) 1858 slider_rep.GetPoint2Coordinate().SetValue(0.45, 0.06) 1859 elif pos == 4: # bottom-right 1860 slider_rep.GetPoint1Coordinate().SetValue(0.55, 0.06) 1861 slider_rep.GetPoint2Coordinate().SetValue(0.95, 0.06) 1862 elif pos == 5: # bottom span horizontal 1863 slider_rep.GetPoint1Coordinate().SetValue(0.04, 0.06) 1864 slider_rep.GetPoint2Coordinate().SetValue(0.95, 0.06) 1865 elif pos == 11: # top-left vertical 1866 slider_rep.GetPoint1Coordinate().SetValue(0.065, 0.54) 1867 slider_rep.GetPoint2Coordinate().SetValue(0.065, 0.9) 1868 elif pos == 12: 1869 slider_rep.GetPoint1Coordinate().SetValue(0.94, 0.54) 1870 slider_rep.GetPoint2Coordinate().SetValue(0.94, 0.9) 1871 elif pos == 13: 1872 slider_rep.GetPoint1Coordinate().SetValue(0.065, 0.1) 1873 slider_rep.GetPoint2Coordinate().SetValue(0.065, 0.54) 1874 elif pos == 14: # bottom-right vertical 1875 slider_rep.GetPoint1Coordinate().SetValue(0.94, 0.1) 1876 slider_rep.GetPoint2Coordinate().SetValue(0.94, 0.54) 1877 elif pos == 15: # right margin vertical 1878 slider_rep.GetPoint1Coordinate().SetValue(0.95, 0.1) 1879 slider_rep.GetPoint2Coordinate().SetValue(0.95, 0.9) 1880 else: # bottom-right 1881 slider_rep.GetPoint1Coordinate().SetValue(0.55, 0.06) 1882 slider_rep.GetPoint2Coordinate().SetValue(0.95, 0.06) 1883 1884 if show_value: 1885 if tformat is None: 1886 if isinstance(xmin, int) and isinstance(xmax, int) and isinstance(value, int): 1887 tformat = "%0.0f" 1888 else: 1889 tformat = "%0.2f" 1890 1891 slider_rep.SetLabelFormat(tformat) # default is '%0.3g' 1892 slider_rep.GetLabelProperty().SetShadow(0) 1893 slider_rep.GetLabelProperty().SetBold(0) 1894 slider_rep.GetLabelProperty().SetOpacity(alpha) 1895 slider_rep.GetLabelProperty().SetColor(c) 1896 if isinstance(pos, int) and pos > 10: 1897 slider_rep.GetLabelProperty().SetOrientation(90) 1898 else: 1899 slider_rep.ShowSliderLabelOff() 1900 slider_rep.GetTubeProperty().SetColor(c) 1901 slider_rep.GetTubeProperty().SetOpacity(0.75) 1902 slider_rep.GetSliderProperty().SetColor(c) 1903 slider_rep.GetSelectedProperty().SetColor(np.sqrt(np.array(c))) 1904 slider_rep.GetCapProperty().SetColor(c) 1905 1906 slider_rep.SetTitleHeight(title_height * title_size) 1907 slider_rep.GetTitleProperty().SetShadow(0) 1908 slider_rep.GetTitleProperty().SetColor(c) 1909 slider_rep.GetTitleProperty().SetOpacity(alpha) 1910 slider_rep.GetTitleProperty().SetBold(0) 1911 if font.lower() == "courier": 1912 slider_rep.GetTitleProperty().SetFontFamilyToCourier() 1913 elif font.lower() == "times": 1914 slider_rep.GetTitleProperty().SetFontFamilyToTimes() 1915 elif font.lower() == "arial": 1916 slider_rep.GetTitleProperty().SetFontFamilyToArial() 1917 else: 1918 if font == "": 1919 font = utils.get_font_path(settings.default_font) 1920 else: 1921 font = utils.get_font_path(font) 1922 slider_rep.GetTitleProperty().SetFontFamily(vtki.VTK_FONT_FILE) 1923 slider_rep.GetLabelProperty().SetFontFamily(vtki.VTK_FONT_FILE) 1924 slider_rep.GetTitleProperty().SetFontFile(font) 1925 slider_rep.GetLabelProperty().SetFontFile(font) 1926 1927 if title: 1928 slider_rep.SetTitleText(title) 1929 if not utils.is_sequence(pos): 1930 if isinstance(pos, int) and pos > 10: 1931 slider_rep.GetTitleProperty().SetOrientation(90) 1932 else: 1933 if abs(pos[0][0] - pos[1][0]) < 0.1: 1934 slider_rep.GetTitleProperty().SetOrientation(90) 1935 1936 super().__init__() 1937 1938 self.SetAnimationModeToJump() 1939 self.SetRepresentation(slider_rep) 1940 if delayed: 1941 self.AddObserver("EndInteractionEvent", sliderfunc) 1942 else: 1943 self.AddObserver("InteractionEvent", sliderfunc) 1944 1945 1946##################################################################### 1947class Slider3D(SliderWidget): 1948 """ 1949 Add a 3D slider which can call an external custom function. 1950 """ 1951 1952 def __init__( 1953 self, 1954 sliderfunc, 1955 pos1, 1956 pos2, 1957 xmin, 1958 xmax, 1959 value=None, 1960 s=0.03, 1961 t=1, 1962 title="", 1963 rotation=0, 1964 c=None, 1965 show_value=True, 1966 ): 1967 """ 1968 Add a 3D slider which can call an external custom function. 1969 1970 Arguments: 1971 sliderfunc : (function) 1972 external function to be called by the widget 1973 pos1 : (list) 1974 first position 3D coordinates 1975 pos2 : (list) 1976 second position 3D coordinates 1977 xmin : (float) 1978 lower value 1979 xmax : (float) 1980 upper value 1981 value : (float) 1982 initial value 1983 s : (float) 1984 label scaling factor 1985 t : (float) 1986 tube scaling factor 1987 title : (str) 1988 title text 1989 c : (color) 1990 slider color 1991 rotation : (float) 1992 title rotation around slider axis 1993 show_value : (bool) 1994 if True current value is shown on top of the slider 1995 1996 Examples: 1997 - [sliders3d.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/sliders3d.py) 1998 """ 1999 c = get_color(c) 2000 2001 if value is None or value < xmin: 2002 value = xmin 2003 2004 slider_rep = vtki.new("SliderRepresentation3D") 2005 slider_rep.SetMinimumValue(xmin) 2006 slider_rep.SetMaximumValue(xmax) 2007 slider_rep.SetValue(value) 2008 2009 slider_rep.GetPoint1Coordinate().SetCoordinateSystemToWorld() 2010 slider_rep.GetPoint2Coordinate().SetCoordinateSystemToWorld() 2011 slider_rep.GetPoint1Coordinate().SetValue(pos2) 2012 slider_rep.GetPoint2Coordinate().SetValue(pos1) 2013 2014 # slider_rep.SetPoint1InWorldCoordinates(pos2[0], pos2[1], pos2[2]) 2015 # slider_rep.SetPoint2InWorldCoordinates(pos1[0], pos1[1], pos1[2]) 2016 2017 slider_rep.SetSliderWidth(0.03 * t) 2018 slider_rep.SetTubeWidth(0.01 * t) 2019 slider_rep.SetSliderLength(0.04 * t) 2020 slider_rep.SetSliderShapeToCylinder() 2021 slider_rep.GetSelectedProperty().SetColor(np.sqrt(np.array(c))) 2022 slider_rep.GetSliderProperty().SetColor(np.array(c) / 1.5) 2023 slider_rep.GetCapProperty().SetOpacity(0) 2024 slider_rep.SetRotation(rotation) 2025 2026 if not show_value: 2027 slider_rep.ShowSliderLabelOff() 2028 2029 slider_rep.SetTitleText(title) 2030 slider_rep.SetTitleHeight(s * t) 2031 slider_rep.SetLabelHeight(s * t * 0.85) 2032 2033 slider_rep.GetTubeProperty().SetColor(c) 2034 2035 super().__init__() 2036 2037 self.SetRepresentation(slider_rep) 2038 self.SetAnimationModeToJump() 2039 self.AddObserver("InteractionEvent", sliderfunc) 2040 2041 2042class BaseCutter: 2043 """ 2044 Base class for Cutter widgets. 2045 """ 2046 2047 def __init__(self): 2048 self._implicit_func = None 2049 self.widget = None 2050 self.clipper = None 2051 self.cutter = None 2052 self.mesh = None 2053 self.remnant = None 2054 self._alpha = 0.5 2055 self._keypress_id = None 2056 2057 def invert(self) -> Self: 2058 """Invert selection.""" 2059 self.clipper.SetInsideOut(not self.clipper.GetInsideOut()) 2060 return self 2061 2062 def bounds(self, value=None) -> Union[Self, np.ndarray]: 2063 """Set or get the bounding box.""" 2064 if value is None: 2065 return self.cutter.GetBounds() 2066 else: 2067 self._implicit_func.SetBounds(value) 2068 return self 2069 2070 def on(self) -> Self: 2071 """Switch the widget on or off.""" 2072 self.widget.On() 2073 return self 2074 2075 def off(self) -> Self: 2076 """Switch the widget on or off.""" 2077 self.widget.Off() 2078 return self 2079 2080 def add_to(self, plt) -> Self: 2081 """Assign the widget to the provided `Plotter` instance.""" 2082 self.widget.SetInteractor(plt.interactor) 2083 self.widget.SetCurrentRenderer(plt.renderer) 2084 if self.widget not in plt.widgets: 2085 plt.widgets.append(self.widget) 2086 2087 cpoly = self.clipper.GetOutput() 2088 self.mesh._update(cpoly) 2089 2090 out = self.clipper.GetClippedOutputPort() 2091 if self._alpha: 2092 self.remnant.mapper.SetInputConnection(out) 2093 self.remnant.alpha(self._alpha).color((0.5, 0.5, 0.5)) 2094 self.remnant.lighting("off").wireframe() 2095 plt.add(self.mesh, self.remnant) 2096 else: 2097 plt.add(self.mesh) 2098 2099 self._keypress_id = plt.interactor.AddObserver( 2100 "KeyPressEvent", self._keypress 2101 ) 2102 if plt.interactor and plt.interactor.GetInitialized(): 2103 self.widget.On() 2104 self._select_polygons(self.widget, "InteractionEvent") 2105 plt.interactor.Render() 2106 return self 2107 2108 def remove_from(self, plt) -> Self: 2109 """Remove the widget to the provided `Plotter` instance.""" 2110 self.widget.Off() 2111 self.widget.RemoveAllObservers() ### NOT SURE 2112 plt.remove(self.remnant) 2113 if self.widget in plt.widgets: 2114 plt.widgets.remove(self.widget) 2115 if self._keypress_id: 2116 plt.interactor.RemoveObserver(self._keypress_id) 2117 return self 2118 2119 def add_observer(self, event, func, priority=1) -> int: 2120 """Add an observer to the widget.""" 2121 event = utils.get_vtk_name_event(event) 2122 cid = self.widget.AddObserver(event, func, priority) 2123 return cid 2124 2125 2126class PlaneCutter(vtki.vtkPlaneWidget, BaseCutter): 2127 """ 2128 Create a box widget to cut away parts of a Mesh. 2129 """ 2130 2131 def __init__( 2132 self, 2133 mesh, 2134 invert=False, 2135 can_translate=True, 2136 can_scale=True, 2137 origin=(), 2138 normal=(), 2139 padding=0.05, 2140 delayed=False, 2141 c=(0.25, 0.25, 0.25), 2142 alpha=0.05, 2143 ): 2144 """ 2145 Create a box widget to cut away parts of a `Mesh`. 2146 2147 Arguments: 2148 mesh : (Mesh) 2149 the input mesh 2150 invert : (bool) 2151 invert the clipping plane 2152 can_translate : (bool) 2153 enable translation of the widget 2154 can_scale : (bool) 2155 enable scaling of the widget 2156 origin : (list) 2157 origin of the plane 2158 normal : (list) 2159 normal to the plane 2160 padding : (float) 2161 padding around the input mesh 2162 delayed : (bool) 2163 if True the callback is delayed until 2164 when the mouse button is released (useful for large meshes) 2165 c : (color) 2166 color of the box cutter widget 2167 alpha : (float) 2168 transparency of the cut-off part of the input mesh 2169 2170 Examples: 2171 - [slice_plane3.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/slice_plane3.py) 2172 """ 2173 super().__init__() 2174 2175 self.mesh = mesh 2176 self.remnant = Mesh() 2177 self.remnant.name = mesh.name + "Remnant" 2178 self.remnant.pickable(False) 2179 2180 self._alpha = alpha 2181 self._keypress_id = None 2182 2183 self._implicit_func = vtki.new("Plane") 2184 2185 poly = mesh.dataset 2186 self.clipper = vtki.new("ClipPolyData") 2187 self.clipper.GenerateClipScalarsOff() 2188 self.clipper.SetInputData(poly) 2189 self.clipper.SetClipFunction(self._implicit_func) 2190 self.clipper.SetInsideOut(invert) 2191 self.clipper.GenerateClippedOutputOn() 2192 self.clipper.Update() 2193 2194 self.widget = vtki.new("ImplicitPlaneWidget") 2195 2196 # self.widget.KeyPressActivationOff() 2197 # self.widget.SetKeyPressActivationValue('i') 2198 2199 self.widget.SetOriginTranslation(can_translate) 2200 self.widget.SetOutlineTranslation(can_translate) 2201 self.widget.SetScaleEnabled(can_scale) 2202 2203 self.widget.GetOutlineProperty().SetColor(get_color(c)) 2204 self.widget.GetOutlineProperty().SetOpacity(0.25) 2205 self.widget.GetOutlineProperty().SetLineWidth(1) 2206 self.widget.GetOutlineProperty().LightingOff() 2207 2208 self.widget.GetSelectedOutlineProperty().SetColor(get_color("red3")) 2209 2210 self.widget.SetTubing(0) 2211 self.widget.SetDrawPlane(bool(alpha)) 2212 self.widget.GetPlaneProperty().LightingOff() 2213 self.widget.GetPlaneProperty().SetOpacity(alpha) 2214 self.widget.GetSelectedPlaneProperty().SetColor(get_color("red5")) 2215 self.widget.GetSelectedPlaneProperty().LightingOff() 2216 2217 self.widget.SetPlaceFactor(1.0 + padding) 2218 self.widget.SetInputData(poly) 2219 self.widget.PlaceWidget() 2220 if delayed: 2221 self.widget.AddObserver("EndInteractionEvent", self._select_polygons) 2222 else: 2223 self.widget.AddObserver("InteractionEvent", self._select_polygons) 2224 2225 if len(origin) == 3: 2226 self.widget.SetOrigin(origin) 2227 else: 2228 self.widget.SetOrigin(mesh.center_of_mass()) 2229 2230 if len(normal) == 3: 2231 self.widget.SetNormal(normal) 2232 else: 2233 self.widget.SetNormal((1, 0, 0)) 2234 2235 @property 2236 def origin(self): 2237 """Get the origin of the plane.""" 2238 return np.array(self.widget.GetOrigin()) 2239 2240 @origin.setter 2241 def origin(self, value): 2242 """Set the origin of the plane.""" 2243 self.widget.SetOrigin(value) 2244 2245 @property 2246 def normal(self): 2247 """Get the normal of the plane.""" 2248 return np.array(self.widget.GetNormal()) 2249 2250 @normal.setter 2251 def normal(self, value): 2252 """Set the normal of the plane.""" 2253 self.widget.SetNormal(value) 2254 2255 def _select_polygons(self, vobj, event) -> None: 2256 vobj.GetPlane(self._implicit_func) 2257 2258 def _keypress(self, vobj, event): 2259 if vobj.GetKeySym() == "r": # reset planes 2260 self.widget.GetPlane(self._implicit_func) 2261 self.widget.PlaceWidget() 2262 self.widget.GetInteractor().Render() 2263 elif vobj.GetKeySym() == "u": # invert cut 2264 self.invert() 2265 self.widget.GetInteractor().Render() 2266 elif vobj.GetKeySym() == "x": # set normal along x 2267 self.widget.SetNormal((1, 0, 0)) 2268 self.widget.GetPlane(self._implicit_func) 2269 self.widget.PlaceWidget() 2270 self.widget.GetInteractor().Render() 2271 elif vobj.GetKeySym() == "y": # set normal along y 2272 self.widget.SetNormal((0, 1, 0)) 2273 self.widget.GetPlane(self._implicit_func) 2274 self.widget.PlaceWidget() 2275 self.widget.GetInteractor().Render() 2276 elif vobj.GetKeySym() == "z": # set normal along z 2277 self.widget.SetNormal((0, 0, 1)) 2278 self.widget.GetPlane(self._implicit_func) 2279 self.widget.PlaceWidget() 2280 self.widget.GetInteractor().Render() 2281 elif vobj.GetKeySym() == "s": # Ctrl+s to save mesh 2282 if self.widget.GetInteractor(): 2283 if self.widget.GetInteractor().GetControlKey(): 2284 self.mesh.write("vedo_clipped.vtk") 2285 printc(":save: saved mesh to vedo_clipped.vtk") 2286 2287 2288class BoxCutter(vtki.vtkBoxWidget, BaseCutter): 2289 """ 2290 Create a box widget to cut away parts of a Mesh. 2291 """ 2292 2293 def __init__( 2294 self, 2295 mesh, 2296 invert=False, 2297 can_rotate=True, 2298 can_translate=True, 2299 can_scale=True, 2300 initial_bounds=(), 2301 padding=0.025, 2302 delayed=False, 2303 c=(0.25, 0.25, 0.25), 2304 alpha=0.05, 2305 ): 2306 """ 2307 Create a box widget to cut away parts of a Mesh. 2308 2309 Arguments: 2310 mesh : (Mesh) 2311 the input mesh 2312 invert : (bool) 2313 invert the clipping plane 2314 can_rotate : (bool) 2315 enable rotation of the widget 2316 can_translate : (bool) 2317 enable translation of the widget 2318 can_scale : (bool) 2319 enable scaling of the widget 2320 initial_bounds : (list) 2321 initial bounds of the box widget 2322 padding : (float) 2323 padding space around the input mesh 2324 delayed : (bool) 2325 if True the callback is delayed until 2326 when the mouse button is released (useful for large meshes) 2327 c : (color) 2328 color of the box cutter widget 2329 alpha : (float) 2330 transparency of the cut-off part of the input mesh 2331 """ 2332 super().__init__() 2333 2334 self.mesh = mesh 2335 self.remnant = Mesh() 2336 self.remnant.name = mesh.name + "Remnant" 2337 self.remnant.pickable(False) 2338 2339 self._alpha = alpha 2340 self._keypress_id = None 2341 self._init_bounds = initial_bounds 2342 if len(self._init_bounds) == 0: 2343 self._init_bounds = mesh.bounds() 2344 else: 2345 self._init_bounds = initial_bounds 2346 2347 self._implicit_func = vtki.new("Planes") 2348 self._implicit_func.SetBounds(self._init_bounds) 2349 2350 poly = mesh.dataset 2351 self.clipper = vtki.new("ClipPolyData") 2352 self.clipper.GenerateClipScalarsOff() 2353 self.clipper.SetInputData(poly) 2354 self.clipper.SetClipFunction(self._implicit_func) 2355 self.clipper.SetInsideOut(not invert) 2356 self.clipper.GenerateClippedOutputOn() 2357 self.clipper.Update() 2358 2359 self.widget = vtki.vtkBoxWidget() 2360 2361 self.widget.SetRotationEnabled(can_rotate) 2362 self.widget.SetTranslationEnabled(can_translate) 2363 self.widget.SetScalingEnabled(can_scale) 2364 2365 self.widget.OutlineCursorWiresOn() 2366 self.widget.GetSelectedOutlineProperty().SetColor(get_color("red3")) 2367 self.widget.GetSelectedHandleProperty().SetColor(get_color("red5")) 2368 2369 self.widget.GetOutlineProperty().SetColor(c) 2370 self.widget.GetOutlineProperty().SetOpacity(1) 2371 self.widget.GetOutlineProperty().SetLineWidth(1) 2372 self.widget.GetOutlineProperty().LightingOff() 2373 2374 self.widget.GetSelectedFaceProperty().LightingOff() 2375 self.widget.GetSelectedFaceProperty().SetOpacity(0.1) 2376 2377 self.widget.SetPlaceFactor(1.0 + padding) 2378 self.widget.SetInputData(poly) 2379 self.widget.PlaceWidget() 2380 if delayed: 2381 self.widget.AddObserver("EndInteractionEvent", self._select_polygons) 2382 else: 2383 self.widget.AddObserver("InteractionEvent", self._select_polygons) 2384 2385 def _select_polygons(self, vobj, event): 2386 vobj.GetPlanes(self._implicit_func) 2387 2388 def _keypress(self, vobj, event): 2389 if vobj.GetKeySym() == "r": # reset planes 2390 self._implicit_func.SetBounds(self._init_bounds) 2391 self.widget.GetPlanes(self._implicit_func) 2392 self.widget.PlaceWidget() 2393 self.widget.GetInteractor().Render() 2394 elif vobj.GetKeySym() == "u": 2395 self.invert() 2396 self.widget.GetInteractor().Render() 2397 elif vobj.GetKeySym() == "s": # Ctrl+s to save mesh 2398 if self.widget.GetInteractor(): 2399 if self.widget.GetInteractor().GetControlKey(): 2400 self.mesh.write("vedo_clipped.vtk") 2401 printc(":save: saved mesh to vedo_clipped.vtk") 2402 2403 2404class SphereCutter(vtki.vtkSphereWidget, BaseCutter): 2405 """ 2406 Create a box widget to cut away parts of a Mesh. 2407 """ 2408 2409 def __init__( 2410 self, 2411 mesh, 2412 invert=False, 2413 can_translate=True, 2414 can_scale=True, 2415 origin=(), 2416 radius=0, 2417 res=60, 2418 delayed=False, 2419 c="white", 2420 alpha=0.05, 2421 ): 2422 """ 2423 Create a box widget to cut away parts of a Mesh. 2424 2425 Arguments: 2426 mesh : Mesh 2427 the input mesh 2428 invert : bool 2429 invert the clipping 2430 can_translate : bool 2431 enable translation of the widget 2432 can_scale : bool 2433 enable scaling of the widget 2434 origin : list 2435 initial position of the sphere widget 2436 radius : float 2437 initial radius of the sphere widget 2438 res : int 2439 resolution of the sphere widget 2440 delayed : bool 2441 if True the cutting callback is delayed until 2442 when the mouse button is released (useful for large meshes) 2443 c : color 2444 color of the box cutter widget 2445 alpha : float 2446 transparency of the cut-off part of the input mesh 2447 """ 2448 super().__init__() 2449 2450 self.mesh = mesh 2451 self.remnant = Mesh() 2452 self.remnant.name = mesh.name + "Remnant" 2453 self.remnant.pickable(False) 2454 2455 self._alpha = alpha 2456 self._keypress_id = None 2457 2458 self._implicit_func = vtki.new("Sphere") 2459 2460 if len(origin) == 3: 2461 self._implicit_func.SetCenter(origin) 2462 else: 2463 origin = mesh.center_of_mass() 2464 self._implicit_func.SetCenter(origin) 2465 2466 if radius > 0: 2467 self._implicit_func.SetRadius(radius) 2468 else: 2469 radius = mesh.average_size() * 2 2470 self._implicit_func.SetRadius(radius) 2471 2472 poly = mesh.dataset 2473 self.clipper = vtki.new("ClipPolyData") 2474 self.clipper.GenerateClipScalarsOff() 2475 self.clipper.SetInputData(poly) 2476 self.clipper.SetClipFunction(self._implicit_func) 2477 self.clipper.SetInsideOut(not invert) 2478 self.clipper.GenerateClippedOutputOn() 2479 self.clipper.Update() 2480 2481 self.widget = vtki.vtkSphereWidget() 2482 2483 self.widget.SetThetaResolution(res * 2) 2484 self.widget.SetPhiResolution(res) 2485 self.widget.SetRadius(radius) 2486 self.widget.SetCenter(origin) 2487 self.widget.SetRepresentation(2) 2488 self.widget.HandleVisibilityOff() 2489 2490 self.widget.SetTranslation(can_translate) 2491 self.widget.SetScale(can_scale) 2492 2493 self.widget.HandleVisibilityOff() 2494 self.widget.GetSphereProperty().SetColor(get_color(c)) 2495 self.widget.GetSphereProperty().SetOpacity(0.2) 2496 self.widget.GetSelectedSphereProperty().SetColor(get_color("red5")) 2497 self.widget.GetSelectedSphereProperty().SetOpacity(0.2) 2498 2499 self.widget.SetPlaceFactor(1.0) 2500 self.widget.SetInputData(poly) 2501 self.widget.PlaceWidget() 2502 if delayed: 2503 self.widget.AddObserver("EndInteractionEvent", self._select_polygons) 2504 else: 2505 self.widget.AddObserver("InteractionEvent", self._select_polygons) 2506 2507 def _select_polygons(self, vobj, event): 2508 vobj.GetSphere(self._implicit_func) 2509 2510 def _keypress(self, vobj, event): 2511 if vobj.GetKeySym() == "r": # reset planes 2512 self._implicit_func.SetBounds(self._init_bounds) 2513 self.widget.GetPlanes(self._implicit_func) 2514 self.widget.PlaceWidget() 2515 self.widget.GetInteractor().Render() 2516 elif vobj.GetKeySym() == "u": 2517 self.invert() 2518 self.widget.GetInteractor().Render() 2519 elif vobj.GetKeySym() == "s": # Ctrl+s to save mesh 2520 if self.widget.GetInteractor(): 2521 if self.widget.GetInteractor().GetControlKey(): 2522 self.mesh.write("vedo_clipped.vtk") 2523 printc(":save: saved mesh to vedo_clipped.vtk") 2524 2525 @property 2526 def center(self): 2527 """Get the center of the sphere.""" 2528 return np.array(self.widget.GetCenter()) 2529 2530 @center.setter 2531 def center(self, value): 2532 """Set the center of the sphere.""" 2533 self.widget.SetCenter(value) 2534 2535 @property 2536 def radius(self): 2537 """Get the radius of the sphere.""" 2538 return self.widget.GetRadius() 2539 2540 @radius.setter 2541 def radius(self, value): 2542 """Set the radius of the sphere.""" 2543 self.widget.SetRadius(value) 2544 2545 2546##################################################################### 2547class RendererFrame(vtki.vtkActor2D): 2548 """ 2549 Add a line around the renderer subwindow. 2550 """ 2551 2552 def __init__(self, c="k", alpha=None, lw=None, padding=None): 2553 """ 2554 Add a line around the renderer subwindow. 2555 2556 Arguments: 2557 c : (color) 2558 color of the line. 2559 alpha : (float) 2560 opacity. 2561 lw : (int) 2562 line width in pixels. 2563 padding : (int) 2564 padding in pixel units. 2565 """ 2566 2567 if lw is None: 2568 lw = settings.renderer_frame_width 2569 if lw == 0: 2570 return None 2571 2572 if alpha is None: 2573 alpha = settings.renderer_frame_alpha 2574 2575 if padding is None: 2576 padding = settings.renderer_frame_padding 2577 2578 c = get_color(c) 2579 2580 ppoints = vtki.vtkPoints() # Generate the polyline 2581 xy = 1 - padding 2582 psqr = [ 2583 [padding, padding], 2584 [padding, xy], 2585 [xy, xy], 2586 [xy, padding], 2587 [padding, padding], 2588 ] 2589 for i, pt in enumerate(psqr): 2590 ppoints.InsertPoint(i, pt[0], pt[1], 0) 2591 lines = vtki.vtkCellArray() 2592 lines.InsertNextCell(len(psqr)) 2593 for i in range(len(psqr)): 2594 lines.InsertCellPoint(i) 2595 pd = vtki.vtkPolyData() 2596 pd.SetPoints(ppoints) 2597 pd.SetLines(lines) 2598 2599 mapper = vtki.new("PolyDataMapper2D") 2600 mapper.SetInputData(pd) 2601 cs = vtki.new("Coordinate") 2602 cs.SetCoordinateSystemToNormalizedViewport() 2603 mapper.SetTransformCoordinate(cs) 2604 2605 super().__init__() 2606 2607 self.GetPositionCoordinate().SetValue(0, 0) 2608 self.GetPosition2Coordinate().SetValue(1, 1) 2609 self.SetMapper(mapper) 2610 self.GetProperty().SetColor(c) 2611 self.GetProperty().SetOpacity(alpha) 2612 self.GetProperty().SetLineWidth(lw) 2613 2614 2615##################################################################### 2616class ProgressBarWidget(vtki.vtkActor2D): 2617 """ 2618 Add a progress bar in the rendering window. 2619 """ 2620 2621 def __init__(self, n=None, c="blue5", alpha=0.8, lw=10, autohide=True): 2622 """ 2623 Add a progress bar window. 2624 2625 Arguments: 2626 n : (int) 2627 number of iterations. 2628 If None, you need to call `update(fraction)` manually. 2629 c : (color) 2630 color of the line. 2631 alpha : (float) 2632 opacity of the line. 2633 lw : (int) 2634 line width in pixels. 2635 autohide : (bool) 2636 if True, hide the progress bar when completed. 2637 """ 2638 self.n = 0 2639 self.iterations = n 2640 self.autohide = autohide 2641 2642 ppoints = vtki.vtkPoints() # Generate the line 2643 psqr = [[0, 0, 0], [1, 0, 0]] 2644 for i, pt in enumerate(psqr): 2645 ppoints.InsertPoint(i, *pt) 2646 lines = vtki.vtkCellArray() 2647 lines.InsertNextCell(len(psqr)) 2648 for i in range(len(psqr)): 2649 lines.InsertCellPoint(i) 2650 pd = vtki.vtkPolyData() 2651 pd.SetPoints(ppoints) 2652 pd.SetLines(lines) 2653 self.dataset = pd 2654 2655 mapper = vtki.new("PolyDataMapper2D") 2656 mapper.SetInputData(pd) 2657 cs = vtki.vtkCoordinate() 2658 cs.SetCoordinateSystemToNormalizedViewport() 2659 mapper.SetTransformCoordinate(cs) 2660 2661 super().__init__() 2662 2663 self.SetMapper(mapper) 2664 self.GetProperty().SetOpacity(alpha) 2665 self.GetProperty().SetColor(get_color(c)) 2666 self.GetProperty().SetLineWidth(lw * 2) 2667 2668 def lw(self, value: int) -> Self: 2669 """Set width.""" 2670 self.GetProperty().SetLineWidth(value * 2) 2671 return self 2672 2673 def c(self, color) -> Self: 2674 """Set color.""" 2675 c = get_color(color) 2676 self.GetProperty().SetColor(c) 2677 return self 2678 2679 def alpha(self, value) -> Self: 2680 """Set opacity.""" 2681 self.GetProperty().SetOpacity(value) 2682 return self 2683 2684 def update(self, fraction=None) -> Self: 2685 """Update progress bar to fraction of the window width.""" 2686 if fraction is None: 2687 if self.iterations is None: 2688 vedo.printc("Error in ProgressBarWindow: must specify iterations", c='r') 2689 return self 2690 self.n += 1 2691 fraction = self.n / self.iterations 2692 2693 if fraction >= 1 and self.autohide: 2694 fraction = 0 2695 2696 psqr = [[0, 0, 0], [fraction, 0, 0]] 2697 vpts = utils.numpy2vtk(psqr, dtype=np.float32) 2698 self.dataset.GetPoints().SetData(vpts) 2699 return self 2700 2701 def reset(self): 2702 """Reset progress bar.""" 2703 self.n = 0 2704 self.update(0) 2705 return self 2706 2707 2708##################################################################### 2709class Icon(vtki.vtkOrientationMarkerWidget): 2710 """ 2711 Add an inset icon mesh into the renderer. 2712 """ 2713 2714 def __init__(self, mesh, pos=3, size=0.08): 2715 """ 2716 Arguments: 2717 pos : (list, int) 2718 icon position in the range [1-4] indicating one of the 4 corners, 2719 or it can be a tuple (x,y) as a fraction of the renderer size. 2720 size : (float) 2721 size of the icon space as fraction of the window size. 2722 2723 Examples: 2724 - [icon.py](https://github.com/marcomusy/vedo/tree/master/examples/other/icon.py) 2725 """ 2726 super().__init__() 2727 2728 try: 2729 self.SetOrientationMarker(mesh.actor) 2730 except AttributeError: 2731 self.SetOrientationMarker(mesh) 2732 2733 if utils.is_sequence(pos): 2734 self.SetViewport(pos[0] - size, pos[1] - size, pos[0] + size, pos[1] + size) 2735 else: 2736 if pos < 2: 2737 self.SetViewport(0, 1 - 2 * size, size * 2, 1) 2738 elif pos == 2: 2739 self.SetViewport(1 - 2 * size, 1 - 2 * size, 1, 1) 2740 elif pos == 3: 2741 self.SetViewport(0, 0, size * 2, size * 2) 2742 elif pos == 4: 2743 self.SetViewport(1 - 2 * size, 0, 1, size * 2) 2744 2745 2746##################################################################### 2747def compute_visible_bounds(objs=None) -> list: 2748 """Calculate max objects bounds and sizes.""" 2749 bns = [] 2750 2751 if objs is None and vedo.plotter_instance: 2752 objs = vedo.plotter_instance.actors 2753 elif not utils.is_sequence(objs): 2754 objs = [objs] 2755 2756 actors = [ob.actor for ob in objs if hasattr(ob, "actor") and ob.actor] 2757 2758 try: 2759 # this block fails for VolumeSlice as vtkImageSlice.GetBounds() returns a pointer.. 2760 # in any case we dont need axes for that one. 2761 for a in actors: 2762 if a and a.GetUseBounds(): 2763 b = a.GetBounds() 2764 if b: 2765 bns.append(b) 2766 if bns: 2767 max_bns = np.max(bns, axis=0) 2768 min_bns = np.min(bns, axis=0) 2769 vbb = [min_bns[0], max_bns[1], min_bns[2], max_bns[3], min_bns[4], max_bns[5]] 2770 elif vedo.plotter_instance: 2771 vbb = list(vedo.plotter_instance.renderer.ComputeVisiblePropBounds()) 2772 max_bns = vbb 2773 min_bns = vbb 2774 sizes = np.array( 2775 [max_bns[1] - min_bns[0], max_bns[3] - min_bns[2], max_bns[5] - min_bns[4]] 2776 ) 2777 return [vbb, sizes, min_bns, max_bns] 2778 2779 except: 2780 return [[0, 0, 0, 0, 0, 0], [0, 0, 0], 0, 0] 2781 2782 2783##################################################################### 2784def Ruler3D( 2785 p1, 2786 p2, 2787 units_scale=1, 2788 label="", 2789 s=None, 2790 font=None, 2791 italic=0, 2792 prefix="", 2793 units="", # eg.'μm' 2794 c=(0.2, 0.1, 0.1), 2795 alpha=1, 2796 lw=1, 2797 precision=3, 2798 label_rotation=0, 2799 axis_rotation=0, 2800 tick_angle=90, 2801) -> Mesh: 2802 """ 2803 Build a 3D ruler to indicate the distance of two points p1 and p2. 2804 2805 Arguments: 2806 label : (str) 2807 alternative fixed label to be shown 2808 units_scale : (float) 2809 factor to scale units (e.g. μm to mm) 2810 s : (float) 2811 size of the label 2812 font : (str) 2813 font face. Check [available fonts here](https://vedo.embl.es/fonts). 2814 italic : (float) 2815 italicness of the font in the range [0,1] 2816 units : (str) 2817 string to be appended to the numeric value 2818 lw : (int) 2819 line width in pixel units 2820 precision : (int) 2821 nr of significant digits to be shown 2822 label_rotation : (float) 2823 initial rotation of the label around the z-axis 2824 axis_rotation : (float) 2825 initial rotation of the line around the main axis 2826 tick_angle : (float) 2827 initial rotation of the line around the main axis 2828 2829 Examples: 2830 - [goniometer.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/goniometer.py) 2831 2832  2833 """ 2834 2835 if units_scale != 1.0 and units == "": 2836 raise ValueError( 2837 "When setting 'units_scale' to a value other than 1, " 2838 + "a 'units' arguments must be specified." 2839 ) 2840 2841 try: 2842 p1 = p1.pos() 2843 except AttributeError: 2844 pass 2845 2846 try: 2847 p2 = p2.pos() 2848 except AttributeError: 2849 pass 2850 2851 if len(p1) == 2: 2852 p1 = [p1[0], p1[1], 0.0] 2853 if len(p2) == 2: 2854 p2 = [p2[0], p2[1], 0.0] 2855 2856 p1, p2 = np.asarray(p1), np.asarray(p2) 2857 q1, q2 = [0, 0, 0], [utils.mag(p2 - p1), 0, 0] 2858 q1, q2 = np.array(q1), np.array(q2) 2859 v = q2 - q1 2860 d = utils.mag(v) * units_scale 2861 2862 pos = np.array(p1) 2863 p1 = p1 - pos 2864 p2 = p2 - pos 2865 2866 if s is None: 2867 s = d * 0.02 * (1 / units_scale) 2868 2869 if not label: 2870 label = str(d) 2871 if precision: 2872 label = utils.precision(d, precision) 2873 if prefix: 2874 label = prefix + "~" + label 2875 if units: 2876 label += "~" + units 2877 2878 lb = shapes.Text3D(label, s=s, font=font, italic=italic, justify="center") 2879 if label_rotation: 2880 lb.rotate_z(label_rotation) 2881 lb.pos((q1 + q2) / 2) 2882 2883 x0, x1 = lb.xbounds() 2884 gap = [(x1 - x0) / 2, 0, 0] 2885 pc1 = (v / 2 - gap) * 0.9 + q1 2886 pc2 = q2 - (v / 2 - gap) * 0.9 2887 2888 lc1 = shapes.Line(q1 - v / 50, pc1).lw(lw) 2889 lc2 = shapes.Line(q2 + v / 50, pc2).lw(lw) 2890 2891 zs = np.array([0, d / 50 * (1 / units_scale), 0]) 2892 ml1 = shapes.Line(-zs, zs).lw(lw) 2893 ml2 = shapes.Line(-zs, zs).lw(lw) 2894 ml1.rotate_z(tick_angle - 90).pos(q1) 2895 ml2.rotate_z(tick_angle - 90).pos(q2) 2896 2897 c1 = shapes.Circle(q1, r=d / 180 * (1 / units_scale), res=24) 2898 c2 = shapes.Circle(q2, r=d / 180 * (1 / units_scale), res=24) 2899 2900 macts = merge(lb, lc1, lc2, c1, c2, ml1, ml2) 2901 macts.c(c).alpha(alpha) 2902 macts.properties.SetLineWidth(lw) 2903 macts.properties.LightingOff() 2904 macts.actor.UseBoundsOff() 2905 macts.rotate_x(axis_rotation) 2906 macts.reorient(q2 - q1, p2 - p1) 2907 macts.pos(pos) 2908 macts.bc("tomato").pickable(False) 2909 return macts 2910 2911 2912def RulerAxes( 2913 inputobj, 2914 xtitle="", 2915 ytitle="", 2916 ztitle="", 2917 xlabel="", 2918 ylabel="", 2919 zlabel="", 2920 xpadding=0.05, 2921 ypadding=0.04, 2922 zpadding=0, 2923 font="Normografo", 2924 s=None, 2925 italic=0, 2926 units="", 2927 c=(0.2, 0, 0), 2928 alpha=1, 2929 lw=1, 2930 precision=3, 2931 label_rotation=0, 2932 xaxis_rotation=0, 2933 yaxis_rotation=0, 2934 zaxis_rotation=0, 2935 xycross=True, 2936) -> Union[Mesh, None]: 2937 """ 2938 A 3D ruler axes to indicate the sizes of the input scene or object. 2939 2940 Arguments: 2941 xtitle : (str) 2942 name of the axis or title 2943 xlabel : (str) 2944 alternative fixed label to be shown instead of the distance 2945 s : (float) 2946 size of the label 2947 font : (str) 2948 font face. Check [available fonts here](https://vedo.embl.es/fonts). 2949 italic : (float) 2950 italicness of the font in the range [0,1] 2951 units : (str) 2952 string to be appended to the numeric value 2953 lw : (int) 2954 line width in pixel units 2955 precision : (int) 2956 nr of significant digits to be shown 2957 label_rotation : (float) 2958 initial rotation of the label around the z-axis 2959 [x,y,z]axis_rotation : (float) 2960 initial rotation of the line around the main axis in degrees 2961 xycross : (bool) 2962 show two back crossing lines in the xy plane 2963 2964 Examples: 2965 - [goniometer.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/goniometer.py) 2966 """ 2967 if utils.is_sequence(inputobj): 2968 x0, x1, y0, y1, z0, z1 = inputobj 2969 else: 2970 x0, x1, y0, y1, z0, z1 = inputobj.bounds() 2971 dx, dy, dz = (y1 - y0) * xpadding, (x1 - x0) * ypadding, (y1 - y0) * zpadding 2972 d = np.sqrt((y1 - y0) ** 2 + (x1 - x0) ** 2 + (z1 - z0) ** 2) 2973 2974 if not d: 2975 return None 2976 2977 if s is None: 2978 s = d / 75 2979 2980 acts, rx, ry = [], None, None 2981 if xtitle is not None and (x1 - x0) / d > 0.1: 2982 rx = Ruler3D( 2983 [x0, y0 - dx, z0], 2984 [x1, y0 - dx, z0], 2985 s=s, 2986 font=font, 2987 precision=precision, 2988 label_rotation=label_rotation, 2989 axis_rotation=xaxis_rotation, 2990 lw=lw, 2991 italic=italic, 2992 prefix=xtitle, 2993 label=xlabel, 2994 units=units, 2995 ) 2996 acts.append(rx) 2997 2998 if ytitle is not None and (y1 - y0) / d > 0.1: 2999 ry = Ruler3D( 3000 [x1 + dy, y0, z0], 3001 [x1 + dy, y1, z0], 3002 s=s, 3003 font=font, 3004 precision=precision, 3005 label_rotation=label_rotation, 3006 axis_rotation=yaxis_rotation, 3007 lw=lw, 3008 italic=italic, 3009 prefix=ytitle, 3010 label=ylabel, 3011 units=units, 3012 ) 3013 acts.append(ry) 3014 3015 if ztitle is not None and (z1 - z0) / d > 0.1: 3016 rz = Ruler3D( 3017 [x0 - dy, y0 + dz, z0], 3018 [x0 - dy, y0 + dz, z1], 3019 s=s, 3020 font=font, 3021 precision=precision, 3022 label_rotation=label_rotation, 3023 axis_rotation=zaxis_rotation + 90, 3024 lw=lw, 3025 italic=italic, 3026 prefix=ztitle, 3027 label=zlabel, 3028 units=units, 3029 ) 3030 acts.append(rz) 3031 3032 if xycross and rx and ry: 3033 lx = shapes.Line([x0, y0, z0], [x0, y1 + dx, z0]) 3034 ly = shapes.Line([x0 - dy, y1, z0], [x1, y1, z0]) 3035 d = min((x1 - x0), (y1 - y0)) / 200 3036 cxy = shapes.Circle([x0, y1, z0], r=d, res=15) 3037 acts.extend([lx, ly, cxy]) 3038 3039 macts = merge(acts) 3040 if not macts: 3041 return None 3042 macts.c(c).alpha(alpha).bc("t") 3043 macts.actor.UseBoundsOff() 3044 macts.actor.PickableOff() 3045 return macts 3046 3047 3048##################################################################### 3049class Ruler2D(vtki.vtkAxisActor2D): 3050 """ 3051 Create a ruler with tick marks, labels and a title. 3052 """ 3053 3054 def __init__( 3055 self, 3056 lw=2, 3057 ticks=True, 3058 labels=False, 3059 c="k", 3060 alpha=1, 3061 title="", 3062 font="Calco", 3063 font_size=24, 3064 bc=None, 3065 ): 3066 """ 3067 Create a ruler with tick marks, labels and a title. 3068 3069 Ruler2D is a 2D actor; that is, it is drawn on the overlay 3070 plane and is not occluded by 3D geometry. 3071 To use this class, specify two points defining the start and end 3072 with update_points() as 3D points. 3073 3074 This class decides decides how to create reasonable tick 3075 marks and labels. 3076 3077 Labels are drawn on the "right" side of the axis. 3078 The "right" side is the side of the axis on the right. 3079 The way the labels and title line up with the axis and tick marks 3080 depends on whether the line is considered horizontal or vertical. 3081 3082 Arguments: 3083 lw : (int) 3084 width of the line in pixel units 3085 ticks : (bool) 3086 control if drawing the tick marks 3087 labels : (bool) 3088 control if drawing the numeric labels 3089 c : (color) 3090 color of the object 3091 alpha : (float) 3092 opacity of the object 3093 title : (str) 3094 title of the ruler 3095 font : (str) 3096 font face name. Check [available fonts here](https://vedo.embl.es/fonts). 3097 font_size : (int) 3098 font size 3099 bc : (color) 3100 background color of the title 3101 3102 Example: 3103 ```python 3104 from vedo import * 3105 plt = Plotter(axes=1, interactive=False) 3106 plt.show(Cube()) 3107 rul = Ruler2D() 3108 rul.set_points([0,0,0], [0.5,0.5,0.5]) 3109 plt.add(rul) 3110 plt.interactive().close() 3111 ``` 3112  3113 """ 3114 super().__init__() 3115 3116 plt = vedo.plotter_instance 3117 if not plt: 3118 vedo.logger.error("Ruler2D need to initialize Plotter first.") 3119 raise RuntimeError() 3120 3121 self.p0 = [0, 0, 0] 3122 self.p1 = [0, 0, 0] 3123 self.distance = 0 3124 self.title = title 3125 3126 prop = self.GetProperty() 3127 tprop = self.GetTitleTextProperty() 3128 3129 self.SetTitle(title) 3130 self.SetNumberOfLabels(9) 3131 3132 if not font: 3133 font = settings.default_font 3134 if font.lower() == "courier": 3135 tprop.SetFontFamilyToCourier() 3136 elif font.lower() == "times": 3137 tprop.SetFontFamilyToTimes() 3138 elif font.lower() == "arial": 3139 tprop.SetFontFamilyToArial() 3140 else: 3141 tprop.SetFontFamily(vtki.VTK_FONT_FILE) 3142 tprop.SetFontFile(utils.get_font_path(font)) 3143 tprop.SetFontSize(font_size) 3144 tprop.BoldOff() 3145 tprop.ItalicOff() 3146 tprop.ShadowOff() 3147 tprop.SetColor(get_color(c)) 3148 tprop.SetOpacity(alpha) 3149 if bc is not None: 3150 bc = get_color(bc) 3151 tprop.SetBackgroundColor(bc) 3152 tprop.SetBackgroundOpacity(alpha) 3153 3154 lprop = vtki.vtkTextProperty() 3155 lprop.ShallowCopy(tprop) 3156 self.SetLabelTextProperty(lprop) 3157 3158 self.SetLabelFormat("%0.3g") 3159 self.SetTickVisibility(ticks) 3160 self.SetLabelVisibility(labels) 3161 prop.SetLineWidth(lw) 3162 prop.SetColor(get_color(c)) 3163 3164 self.renderer = plt.renderer 3165 self.cid = plt.interactor.AddObserver("RenderEvent", self._update_viz, 1.0) 3166 3167 def color(self, c) -> Self: 3168 """Assign a new color.""" 3169 c = get_color(c) 3170 self.GetTitleTextProperty().SetColor(c) 3171 self.GetLabelTextProperty().SetColor(c) 3172 self.GetProperty().SetColor(c) 3173 return self 3174 3175 def off(self) -> None: 3176 """Switch off the ruler completely.""" 3177 self.renderer.RemoveObserver(self.cid) 3178 self.renderer.RemoveActor(self) 3179 3180 def set_points(self, p0, p1) -> Self: 3181 """Set new values for the ruler start and end points.""" 3182 self.p0 = np.asarray(p0) 3183 self.p1 = np.asarray(p1) 3184 self._update_viz(0, 0) 3185 return self 3186 3187 def _update_viz(self, evt, name) -> None: 3188 ren = self.renderer 3189 view_size = np.array(ren.GetSize()) 3190 3191 ren.SetWorldPoint(*self.p0, 1) 3192 ren.WorldToDisplay() 3193 disp_point1 = ren.GetDisplayPoint()[:2] 3194 disp_point1 = np.array(disp_point1) / view_size 3195 3196 ren.SetWorldPoint(*self.p1, 1) 3197 ren.WorldToDisplay() 3198 disp_point2 = ren.GetDisplayPoint()[:2] 3199 disp_point2 = np.array(disp_point2) / view_size 3200 3201 self.SetPoint1(*disp_point1) 3202 self.SetPoint2(*disp_point2) 3203 self.distance = np.linalg.norm(self.p1 - self.p0) 3204 self.SetRange(0.0, float(self.distance)) 3205 if not self.title: 3206 self.SetTitle(utils.precision(self.distance, 3)) 3207 3208 3209##################################################################### 3210class DistanceTool(Group): 3211 """ 3212 Create a tool to measure the distance between two clicked points. 3213 """ 3214 3215 def __init__(self, plotter=None, c="k", lw=2): 3216 """ 3217 Create a tool to measure the distance between two clicked points. 3218 3219 Example: 3220 ```python 3221 from vedo import * 3222 mesh = ParametricShape("RandomHills").c("red5") 3223 plt = Plotter(axes=1) 3224 dtool = DistanceTool() 3225 dtool.on() 3226 plt.show(mesh, dtool) 3227 dtool.off() 3228 ``` 3229  3230 """ 3231 super().__init__() 3232 3233 self.p0 = [0, 0, 0] 3234 self.p1 = [0, 0, 0] 3235 self.distance = 0 3236 if plotter is None: 3237 plotter = vedo.plotter_instance 3238 self.plotter = plotter 3239 self.callback = None 3240 self.cid = None 3241 self.color = c 3242 self.linewidth = lw 3243 self.toggle = True 3244 self.ruler = None 3245 self.title = "" 3246 3247 def on(self) -> Self: 3248 """Switch tool on.""" 3249 self.cid = self.plotter.add_callback("click", self._onclick) 3250 self.VisibilityOn() 3251 self.plotter.render() 3252 return self 3253 3254 def off(self) -> None: 3255 """Switch tool off.""" 3256 self.plotter.remove_callback(self.cid) 3257 self.VisibilityOff() 3258 self.ruler.off() 3259 self.plotter.render() 3260 3261 def _onclick(self, event): 3262 if not event.actor: 3263 return 3264 3265 self.clear() 3266 3267 acts = [] 3268 if self.toggle: 3269 self.p0 = event.picked3d 3270 acts.append(Point(self.p0, c=self.color)) 3271 else: 3272 self.p1 = event.picked3d 3273 self.distance = np.linalg.norm(self.p1 - self.p0) 3274 acts.append(Point(self.p0, c=self.color)) 3275 acts.append(Point(self.p1, c=self.color)) 3276 self.ruler = Ruler2D(c=self.color) 3277 self.ruler.set_points(self.p0, self.p1) 3278 acts.append(self.ruler) 3279 3280 if self.callback is not None: 3281 self.callback(event) 3282 3283 for a in acts: 3284 try: 3285 self += a.actor 3286 except AttributeError: 3287 self += a 3288 self.toggle = not self.toggle 3289 3290 3291##################################################################### 3292def Axes( 3293 obj=None, 3294 xtitle='x', ytitle='y', ztitle='z', 3295 xrange=None, yrange=None, zrange=None, 3296 c=None, 3297 number_of_divisions=None, 3298 digits=None, 3299 limit_ratio=0.04, 3300 title_depth=0, 3301 title_font="", # grab settings.default_font 3302 text_scale=1.0, 3303 x_values_and_labels=None, y_values_and_labels=None, z_values_and_labels=None, 3304 htitle="", 3305 htitle_size=0.03, 3306 htitle_font=None, 3307 htitle_italic=False, 3308 htitle_color=None, htitle_backface_color=None, 3309 htitle_justify='bottom-left', 3310 htitle_rotation=0, 3311 htitle_offset=(0, 0.01, 0), 3312 xtitle_position=0.95, ytitle_position=0.95, ztitle_position=0.95, 3313 # xtitle_offset can be a list (dx,dy,dz) 3314 xtitle_offset=0.025, ytitle_offset=0.0275, ztitle_offset=0.02, 3315 xtitle_justify=None, ytitle_justify=None, ztitle_justify=None, 3316 # xtitle_rotation can be a list (rx,ry,rz) 3317 xtitle_rotation=0, ytitle_rotation=0, ztitle_rotation=0, 3318 xtitle_box=False, ytitle_box=False, 3319 xtitle_size=0.025, ytitle_size=0.025, ztitle_size=0.025, 3320 xtitle_color=None, ytitle_color=None, ztitle_color=None, 3321 xtitle_backface_color=None, ytitle_backface_color=None, ztitle_backface_color=None, 3322 xtitle_italic=0, ytitle_italic=0, ztitle_italic=0, 3323 grid_linewidth=1, 3324 xygrid=True, yzgrid=False, zxgrid=False, 3325 xygrid2=False, yzgrid2=False, zxgrid2=False, 3326 xygrid_transparent=False, yzgrid_transparent=False, zxgrid_transparent=False, 3327 xygrid2_transparent=False, yzgrid2_transparent=False, zxgrid2_transparent=False, 3328 xyplane_color=None, yzplane_color=None, zxplane_color=None, 3329 xygrid_color=None, yzgrid_color=None, zxgrid_color=None, 3330 xyalpha=0.075, yzalpha=0.075, zxalpha=0.075, 3331 xyframe_line=None, yzframe_line=None, zxframe_line=None, 3332 xyframe_color=None, yzframe_color=None, zxframe_color=None, 3333 axes_linewidth=1, 3334 xline_color=None, yline_color=None, zline_color=None, 3335 xhighlight_zero=False, yhighlight_zero=False, zhighlight_zero=False, 3336 xhighlight_zero_color='red4', yhighlight_zero_color='green4', zhighlight_zero_color='blue4', 3337 show_ticks=True, 3338 xtick_length=0.015, ytick_length=0.015, ztick_length=0.015, 3339 xtick_thickness=0.0025, ytick_thickness=0.0025, ztick_thickness=0.0025, 3340 xminor_ticks=1, yminor_ticks=1, zminor_ticks=1, 3341 tip_size=None, 3342 label_font="", # grab settings.default_font 3343 xlabel_color=None, ylabel_color=None, zlabel_color=None, 3344 xlabel_backface_color=None, ylabel_backface_color=None, zlabel_backface_color=None, 3345 xlabel_size=0.016, ylabel_size=0.016, zlabel_size=0.016, 3346 xlabel_offset=0.8, ylabel_offset=0.8, zlabel_offset=0.8, # each can be a list (dx,dy,dz) 3347 xlabel_justify=None, ylabel_justify=None, zlabel_justify=None, 3348 xlabel_rotation=0, ylabel_rotation=0, zlabel_rotation=0, # each can be a list (rx,ry,rz) 3349 xaxis_rotation=0, yaxis_rotation=0, zaxis_rotation=0, # rotate all elements around axis 3350 xyshift=0, yzshift=0, zxshift=0, 3351 xshift_along_y=0, xshift_along_z=0, 3352 yshift_along_x=0, yshift_along_z=0, 3353 zshift_along_x=0, zshift_along_y=0, 3354 x_use_bounds=True, y_use_bounds=True, z_use_bounds=False, 3355 x_inverted=False, y_inverted=False, z_inverted=False, 3356 use_global=False, 3357 tol=0.001, 3358 ) -> Union[Assembly, None]: 3359 """ 3360 Draw axes for the input object. 3361 Check [available fonts here](https://vedo.embl.es/fonts). 3362 3363 Returns an `vedo.Assembly` object. 3364 3365 Parameters 3366 ---------- 3367 3368 - `xtitle`, ['x'], x-axis title text 3369 - `xrange`, [None], x-axis range in format (xmin, ymin), default is automatic. 3370 - `number_of_divisions`, [None], approximate number of divisions on the longest axis 3371 - `axes_linewidth`, [1], width of the axes lines 3372 - `grid_linewidth`, [1], width of the grid lines 3373 - `title_depth`, [0], extrusion fractional depth of title text 3374 - `x_values_and_labels` [], assign custom tick positions and labels [(pos1, label1), ...] 3375 - `xygrid`, [True], show a gridded wall on plane xy 3376 - `yzgrid`, [True], show a gridded wall on plane yz 3377 - `zxgrid`, [True], show a gridded wall on plane zx 3378 - `yzgrid2`, [False], show yz plane on opposite side of the bounding box 3379 - `zxgrid2`, [False], show zx plane on opposite side of the bounding box 3380 - `xygrid_transparent` [False], make grid plane completely transparent 3381 - `xygrid2_transparent` [False], make grid plane completely transparent on opposite side box 3382 - `xyplane_color`, ['None'], color of the plane 3383 - `xygrid_color`, ['None'], grid line color 3384 - `xyalpha`, [0.15], grid plane opacity 3385 - `xyframe_line`, [0], add a frame for the plane, use value as the thickness 3386 - `xyframe_color`, [None], color for the frame of the plane 3387 - `show_ticks`, [True], show major ticks 3388 - `digits`, [None], use this number of significant digits in scientific notation 3389 - `title_font`, [''], font for axes titles 3390 - `label_font`, [''], font for numeric labels 3391 - `text_scale`, [1.0], global scaling factor for all text elements (titles, labels) 3392 - `htitle`, [''], header title 3393 - `htitle_size`, [0.03], header title size 3394 - `htitle_font`, [None], header font (defaults to `title_font`) 3395 - `htitle_italic`, [True], header font is italic 3396 - `htitle_color`, [None], header title color (defaults to `xtitle_color`) 3397 - `htitle_backface_color`, [None], header title color on its backface 3398 - `htitle_justify`, ['bottom-center'], origin of the title justification 3399 - `htitle_offset`, [(0,0.01,0)], control offsets of header title in x, y and z 3400 - `xtitle_position`, [0.32], title fractional positions along axis 3401 - `xtitle_offset`, [0.05], title fractional offset distance from axis line, can be a list 3402 - `xtitle_justify`, [None], choose the origin of the bounding box of title 3403 - `xtitle_rotation`, [0], add a rotation of the axis title, can be a list (rx,ry,rz) 3404 - `xtitle_box`, [False], add a box around title text 3405 - `xline_color`, [automatic], color of the x-axis 3406 - `xtitle_color`, [automatic], color of the axis title 3407 - `xtitle_backface_color`, [None], color of axis title on its backface 3408 - `xtitle_size`, [0.025], size of the axis title 3409 - `xtitle_italic`, [0], a bool or float to make the font italic 3410 - `xhighlight_zero`, [True], draw a line highlighting zero position if in range 3411 - `xhighlight_zero_color`, [auto], color of the line highlighting the zero position 3412 - `xtick_length`, [0.005], radius of the major ticks 3413 - `xtick_thickness`, [0.0025], thickness of the major ticks along their axis 3414 - `xminor_ticks`, [1], number of minor ticks between two major ticks 3415 - `xlabel_color`, [automatic], color of numeric labels and ticks 3416 - `xlabel_backface_color`, [auto], back face color of numeric labels and ticks 3417 - `xlabel_size`, [0.015], size of the numeric labels along axis 3418 - `xlabel_rotation`, [0,list], numeric labels rotation (can be a list of 3 rotations) 3419 - `xlabel_offset`, [0.8,list], offset of the numeric labels (can be a list of 3 offsets) 3420 - `xlabel_justify`, [None], choose the origin of the bounding box of labels 3421 - `xaxis_rotation`, [0], rotate the X axis elements (ticks and labels) around this same axis 3422 - `xyshift` [0.0], slide the xy-plane along z (the range is [0,1]) 3423 - `xshift_along_y` [0.0], slide x-axis along the y-axis (the range is [0,1]) 3424 - `tip_size`, [0.01], size of the arrow tip as a fraction of the bounding box diagonal 3425 - `limit_ratio`, [0.04], below this ratio don't plot smaller axis 3426 - `x_use_bounds`, [True], keep into account space occupied by labels when setting camera 3427 - `x_inverted`, [False], invert labels order and direction (only visually!) 3428 - `use_global`, [False], try to compute the global bounding box of visible actors 3429 3430 Example: 3431 ```python 3432 from vedo import Axes, Box, show 3433 box = Box(pos=(1,2,3), length=8, width=9, height=7).alpha(0.1) 3434 axs = Axes(box, c='k') # returns an Assembly object 3435 for a in axs.unpack(): 3436 print(a.name) 3437 show(box, axs).close() 3438 ``` 3439  3440 3441 Examples: 3442 - [custom_axes1.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/custom_axes1.py) 3443 - [custom_axes2.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/custom_axes2.py) 3444 - [custom_axes3.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/custom_axes3.py) 3445 - [custom_axes4.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/custom_axes4.py) 3446 3447  3448 """ 3449 if not title_font: 3450 title_font = vedo.settings.default_font 3451 if not label_font: 3452 label_font = vedo.settings.default_font 3453 3454 if c is None: # automatic black or white 3455 c = (0.1, 0.1, 0.1) 3456 plt = vedo.plotter_instance 3457 if plt and plt.renderer: 3458 bgcol = plt.renderer.GetBackground() 3459 else: 3460 bgcol = (1, 1, 1) 3461 if np.sum(bgcol) < 1.5: 3462 c = (0.9, 0.9, 0.9) 3463 else: 3464 c = get_color(c) 3465 3466 # Check if obj has bounds, if so use those 3467 if obj is not None: 3468 try: 3469 bb = obj.bounds() 3470 except AttributeError: 3471 try: 3472 bb = obj.GetBounds() 3473 if xrange is None: xrange = (bb[0], bb[1]) 3474 if yrange is None: yrange = (bb[2], bb[3]) 3475 if zrange is None: zrange = (bb[4], bb[5]) 3476 obj = None # dont need it anymore 3477 except AttributeError: 3478 pass 3479 if utils.is_sequence(obj) and len(obj) == 6 and utils.is_number(obj[0]): 3480 # passing a list of numeric bounds 3481 if xrange is None: xrange = (obj[0], obj[1]) 3482 if yrange is None: yrange = (obj[2], obj[3]) 3483 if zrange is None: zrange = (obj[4], obj[5]) 3484 3485 if use_global: 3486 vbb, drange, min_bns, max_bns = compute_visible_bounds() 3487 else: 3488 if obj is not None: 3489 vbb, drange, min_bns, max_bns = compute_visible_bounds(obj) 3490 else: 3491 vbb = np.zeros(6) 3492 drange = np.zeros(3) 3493 if zrange is None: 3494 zrange = (0, 0) 3495 if xrange is None or yrange is None: 3496 vedo.logger.error("in Axes() must specify axes ranges!") 3497 return None ########################################### 3498 3499 if xrange is not None: 3500 if xrange[1] < xrange[0]: 3501 x_inverted = True 3502 xrange = [xrange[1], xrange[0]] 3503 vbb[0], vbb[1] = xrange 3504 drange[0] = vbb[1] - vbb[0] 3505 min_bns = vbb 3506 max_bns = vbb 3507 if yrange is not None: 3508 if yrange[1] < yrange[0]: 3509 y_inverted = True 3510 yrange = [yrange[1], yrange[0]] 3511 vbb[2], vbb[3] = yrange 3512 drange[1] = vbb[3] - vbb[2] 3513 min_bns = vbb 3514 max_bns = vbb 3515 if zrange is not None: 3516 if zrange[1] < zrange[0]: 3517 z_inverted = True 3518 zrange = [zrange[1], zrange[0]] 3519 vbb[4], vbb[5] = zrange 3520 drange[2] = vbb[5] - vbb[4] 3521 min_bns = vbb 3522 max_bns = vbb 3523 3524 drangemax = max(drange) 3525 if not drangemax: 3526 return None 3527 3528 if drange[0] / drangemax < limit_ratio: 3529 drange[0] = 0 3530 xtitle = "" 3531 if drange[1] / drangemax < limit_ratio: 3532 drange[1] = 0 3533 ytitle = "" 3534 if drange[2] / drangemax < limit_ratio: 3535 drange[2] = 0 3536 ztitle = "" 3537 3538 x0, x1, y0, y1, z0, z1 = vbb 3539 dx, dy, dz = drange 3540 3541 gscale = np.sqrt(dx * dx + dy * dy + dz * dz) * 0.75 3542 3543 if not xyplane_color: xyplane_color = c 3544 if not yzplane_color: yzplane_color = c 3545 if not zxplane_color: zxplane_color = c 3546 if not xygrid_color: xygrid_color = c 3547 if not yzgrid_color: yzgrid_color = c 3548 if not zxgrid_color: zxgrid_color = c 3549 if not xtitle_color: xtitle_color = c 3550 if not ytitle_color: ytitle_color = c 3551 if not ztitle_color: ztitle_color = c 3552 if not xline_color: xline_color = c 3553 if not yline_color: yline_color = c 3554 if not zline_color: zline_color = c 3555 if not xlabel_color: xlabel_color = xline_color 3556 if not ylabel_color: ylabel_color = yline_color 3557 if not zlabel_color: zlabel_color = zline_color 3558 3559 if tip_size is None: 3560 tip_size = 0.005 * gscale 3561 if not ztitle: 3562 tip_size = 0 # switch off in xy 2d 3563 3564 ndiv = 4 3565 if not ztitle or not ytitle or not xtitle: # make more default ticks if 2D 3566 ndiv = 6 3567 if not ztitle: 3568 if xyframe_line is None: 3569 xyframe_line = True 3570 if tip_size is None: 3571 tip_size = False 3572 3573 if utils.is_sequence(number_of_divisions): 3574 rx, ry, rz = number_of_divisions 3575 else: 3576 if not number_of_divisions: 3577 number_of_divisions = ndiv 3578 if not drangemax or np.any(np.isnan(drange)): 3579 rx, ry, rz = 1, 1, 1 3580 else: 3581 rx, ry, rz = np.ceil(drange / drangemax * number_of_divisions).astype(int) 3582 3583 if xtitle: 3584 xticks_float, xticks_str = utils.make_ticks(x0, x1, rx, x_values_and_labels, digits) 3585 xticks_float = xticks_float * dx 3586 if x_inverted: 3587 xticks_float = np.flip(-(xticks_float - xticks_float[-1])) 3588 xticks_str = list(reversed(xticks_str)) 3589 xticks_str[-1] = "" 3590 xhighlight_zero = False 3591 if ytitle: 3592 yticks_float, yticks_str = utils.make_ticks(y0, y1, ry, y_values_and_labels, digits) 3593 yticks_float = yticks_float * dy 3594 if y_inverted: 3595 yticks_float = np.flip(-(yticks_float - yticks_float[-1])) 3596 yticks_str = list(reversed(yticks_str)) 3597 yticks_str[-1] = "" 3598 yhighlight_zero = False 3599 if ztitle: 3600 zticks_float, zticks_str = utils.make_ticks(z0, z1, rz, z_values_and_labels, digits) 3601 zticks_float = zticks_float * dz 3602 if z_inverted: 3603 zticks_float = np.flip(-(zticks_float - zticks_float[-1])) 3604 zticks_str = list(reversed(zticks_str)) 3605 zticks_str[-1] = "" 3606 zhighlight_zero = False 3607 3608 ################################################ axes lines 3609 lines = [] 3610 if xtitle: 3611 axlinex = shapes.Line([0,0,0], [dx,0,0], c=xline_color, lw=axes_linewidth) 3612 axlinex.shift([0, zxshift*dy + xshift_along_y*dy, xyshift*dz + xshift_along_z*dz]) 3613 axlinex.name = 'xAxis' 3614 lines.append(axlinex) 3615 if ytitle: 3616 axliney = shapes.Line([0,0,0], [0,dy,0], c=yline_color, lw=axes_linewidth) 3617 axliney.shift([yzshift*dx + yshift_along_x*dx, 0, xyshift*dz + yshift_along_z*dz]) 3618 axliney.name = 'yAxis' 3619 lines.append(axliney) 3620 if ztitle: 3621 axlinez = shapes.Line([0,0,0], [0,0,dz], c=zline_color, lw=axes_linewidth) 3622 axlinez.shift([yzshift*dx + zshift_along_x*dx, zxshift*dy + zshift_along_y*dy, 0]) 3623 axlinez.name = 'zAxis' 3624 lines.append(axlinez) 3625 3626 ################################################ grid planes 3627 # all shapes have a name to keep track of them in the Assembly 3628 # if user wants to unpack it 3629 grids = [] 3630 if xygrid and xtitle and ytitle: 3631 if not xygrid_transparent: 3632 gxy = shapes.Grid(s=(xticks_float, yticks_float)) 3633 gxy.alpha(xyalpha).c(xyplane_color).lw(0) 3634 if xyshift: gxy.shift([0,0,xyshift*dz]) 3635 elif tol: gxy.shift([0,0,-tol*gscale]) 3636 gxy.name = "xyGrid" 3637 grids.append(gxy) 3638 if grid_linewidth: 3639 gxy_lines = shapes.Grid(s=(xticks_float, yticks_float)) 3640 gxy_lines.c(xyplane_color).lw(grid_linewidth).alpha(xyalpha) 3641 if xyshift: gxy_lines.shift([0,0,xyshift*dz]) 3642 elif tol: gxy_lines.shift([0,0,-tol*gscale]) 3643 gxy_lines.name = "xyGridLines" 3644 grids.append(gxy_lines) 3645 3646 if yzgrid and ytitle and ztitle: 3647 if not yzgrid_transparent: 3648 gyz = shapes.Grid(s=(zticks_float, yticks_float)) 3649 gyz.alpha(yzalpha).c(yzplane_color).lw(0).rotate_y(-90) 3650 if yzshift: gyz.shift([yzshift*dx,0,0]) 3651 elif tol: gyz.shift([-tol*gscale,0,0]) 3652 gyz.name = "yzGrid" 3653 grids.append(gyz) 3654 if grid_linewidth: 3655 gyz_lines = shapes.Grid(s=(zticks_float, yticks_float)) 3656 gyz_lines.c(yzplane_color).lw(grid_linewidth).alpha(yzalpha).rotate_y(-90) 3657 if yzshift: gyz_lines.shift([yzshift*dx,0,0]) 3658 elif tol: gyz_lines.shift([-tol*gscale,0,0]) 3659 gyz_lines.name = "yzGridLines" 3660 grids.append(gyz_lines) 3661 3662 if zxgrid and ztitle and xtitle: 3663 if not zxgrid_transparent: 3664 gzx = shapes.Grid(s=(xticks_float, zticks_float)) 3665 gzx.alpha(zxalpha).c(zxplane_color).lw(0).rotate_x(90) 3666 if zxshift: gzx.shift([0,zxshift*dy,0]) 3667 elif tol: gzx.shift([0,-tol*gscale,0]) 3668 gzx.name = "zxGrid" 3669 grids.append(gzx) 3670 if grid_linewidth: 3671 gzx_lines = shapes.Grid(s=(xticks_float, zticks_float)) 3672 gzx_lines.c(zxplane_color).lw(grid_linewidth).alpha(zxalpha).rotate_x(90) 3673 if zxshift: gzx_lines.shift([0,zxshift*dy,0]) 3674 elif tol: gzx_lines.shift([0,-tol*gscale,0]) 3675 gzx_lines.name = "zxGridLines" 3676 grids.append(gzx_lines) 3677 3678 # Grid2 3679 if xygrid2 and xtitle and ytitle: 3680 if not xygrid2_transparent: 3681 gxy2 = shapes.Grid(s=(xticks_float, yticks_float)).z(dz) 3682 gxy2.alpha(xyalpha).c(xyplane_color).lw(0) 3683 gxy2.shift([0, tol * gscale, 0]) 3684 gxy2.name = "xyGrid2" 3685 grids.append(gxy2) 3686 if grid_linewidth: 3687 gxy2_lines = shapes.Grid(s=(xticks_float, yticks_float)).z(dz) 3688 gxy2_lines.c(xyplane_color).lw(grid_linewidth).alpha(xyalpha) 3689 gxy2_lines.shift([0, tol * gscale, 0]) 3690 gxy2_lines.name = "xygrid2Lines" 3691 grids.append(gxy2_lines) 3692 3693 if yzgrid2 and ytitle and ztitle: 3694 if not yzgrid2_transparent: 3695 gyz2 = shapes.Grid(s=(zticks_float, yticks_float)) 3696 gyz2.alpha(yzalpha).c(yzplane_color).lw(0) 3697 gyz2.rotate_y(-90).x(dx).shift([tol * gscale, 0, 0]) 3698 gyz2.name = "yzGrid2" 3699 grids.append(gyz2) 3700 if grid_linewidth: 3701 gyz2_lines = shapes.Grid(s=(zticks_float, yticks_float)) 3702 gyz2_lines.c(yzplane_color).lw(grid_linewidth).alpha(yzalpha) 3703 gyz2_lines.rotate_y(-90).x(dx).shift([tol * gscale, 0, 0]) 3704 gyz2_lines.name = "yzGrid2Lines" 3705 grids.append(gyz2_lines) 3706 3707 if zxgrid2 and ztitle and xtitle: 3708 if not zxgrid2_transparent: 3709 gzx2 = shapes.Grid(s=(xticks_float, zticks_float)) 3710 gzx2.alpha(zxalpha).c(zxplane_color).lw(0) 3711 gzx2.rotate_x(90).y(dy).shift([0, tol * gscale, 0]) 3712 gzx2.name = "zxGrid2" 3713 grids.append(gzx2) 3714 if grid_linewidth: 3715 gzx2_lines = shapes.Grid(s=(xticks_float, zticks_float)) 3716 gzx2_lines.c(zxplane_color).lw(grid_linewidth).alpha(zxalpha) 3717 gzx2_lines.rotate_x(90).y(dy).shift([0, tol * gscale, 0]) 3718 gzx2_lines.name = "zxGrid2Lines" 3719 grids.append(gzx2_lines) 3720 3721 ################################################ frame lines 3722 framelines = [] 3723 if xyframe_line and xtitle and ytitle: 3724 if not xyframe_color: 3725 xyframe_color = xygrid_color 3726 frxy = shapes.Line( 3727 [[0, dy, 0], [dx, dy, 0], [dx, 0, 0], [0, 0, 0], [0, dy, 0]], 3728 c=xyframe_color, 3729 lw=xyframe_line, 3730 ) 3731 frxy.shift([0, 0, xyshift * dz]) 3732 frxy.name = "xyFrameLine" 3733 framelines.append(frxy) 3734 if yzframe_line and ytitle and ztitle: 3735 if not yzframe_color: 3736 yzframe_color = yzgrid_color 3737 fryz = shapes.Line( 3738 [[0, 0, dz], [0, dy, dz], [0, dy, 0], [0, 0, 0], [0, 0, dz]], 3739 c=yzframe_color, 3740 lw=yzframe_line, 3741 ) 3742 fryz.shift([yzshift * dx, 0, 0]) 3743 fryz.name = "yzFrameLine" 3744 framelines.append(fryz) 3745 if zxframe_line and ztitle and xtitle: 3746 if not zxframe_color: 3747 zxframe_color = zxgrid_color 3748 frzx = shapes.Line( 3749 [[0, 0, dz], [dx, 0, dz], [dx, 0, 0], [0, 0, 0], [0, 0, dz]], 3750 c=zxframe_color, 3751 lw=zxframe_line, 3752 ) 3753 frzx.shift([0, zxshift * dy, 0]) 3754 frzx.name = "zxFrameLine" 3755 framelines.append(frzx) 3756 3757 ################################################ zero lines highlights 3758 highlights = [] 3759 if xygrid and xtitle and ytitle: 3760 if xhighlight_zero and min_bns[0] <= 0 and max_bns[1] > 0: 3761 xhl = -min_bns[0] 3762 hxy = shapes.Line([xhl, 0, 0], [xhl, dy, 0], c=xhighlight_zero_color) 3763 hxy.alpha(np.sqrt(xyalpha)).lw(grid_linewidth * 2) 3764 hxy.shift([0, 0, xyshift * dz]) 3765 hxy.name = "xyHighlightZero" 3766 highlights.append(hxy) 3767 if yhighlight_zero and min_bns[2] <= 0 and max_bns[3] > 0: 3768 yhl = -min_bns[2] 3769 hyx = shapes.Line([0, yhl, 0], [dx, yhl, 0], c=yhighlight_zero_color) 3770 hyx.alpha(np.sqrt(yzalpha)).lw(grid_linewidth * 2) 3771 hyx.shift([0, 0, xyshift * dz]) 3772 hyx.name = "yxHighlightZero" 3773 highlights.append(hyx) 3774 3775 if yzgrid and ytitle and ztitle: 3776 if yhighlight_zero and min_bns[2] <= 0 and max_bns[3] > 0: 3777 yhl = -min_bns[2] 3778 hyz = shapes.Line([0, yhl, 0], [0, yhl, dz], c=yhighlight_zero_color) 3779 hyz.alpha(np.sqrt(yzalpha)).lw(grid_linewidth * 2) 3780 hyz.shift([yzshift * dx, 0, 0]) 3781 hyz.name = "yzHighlightZero" 3782 highlights.append(hyz) 3783 if zhighlight_zero and min_bns[4] <= 0 and max_bns[5] > 0: 3784 zhl = -min_bns[4] 3785 hzy = shapes.Line([0, 0, zhl], [0, dy, zhl], c=zhighlight_zero_color) 3786 hzy.alpha(np.sqrt(yzalpha)).lw(grid_linewidth * 2) 3787 hzy.shift([yzshift * dx, 0, 0]) 3788 hzy.name = "zyHighlightZero" 3789 highlights.append(hzy) 3790 3791 if zxgrid and ztitle and xtitle: 3792 if zhighlight_zero and min_bns[4] <= 0 and max_bns[5] > 0: 3793 zhl = -min_bns[4] 3794 hzx = shapes.Line([0, 0, zhl], [dx, 0, zhl], c=zhighlight_zero_color) 3795 hzx.alpha(np.sqrt(zxalpha)).lw(grid_linewidth * 2) 3796 hzx.shift([0, zxshift * dy, 0]) 3797 hzx.name = "zxHighlightZero" 3798 highlights.append(hzx) 3799 if xhighlight_zero and min_bns[0] <= 0 and max_bns[1] > 0: 3800 xhl = -min_bns[0] 3801 hxz = shapes.Line([xhl, 0, 0], [xhl, 0, dz], c=xhighlight_zero_color) 3802 hxz.alpha(np.sqrt(zxalpha)).lw(grid_linewidth * 2) 3803 hxz.shift([0, zxshift * dy, 0]) 3804 hxz.name = "xzHighlightZero" 3805 highlights.append(hxz) 3806 3807 ################################################ arrow cone 3808 cones = [] 3809 3810 if tip_size: 3811 3812 if xtitle: 3813 if x_inverted: 3814 cx = shapes.Cone( 3815 r=tip_size, 3816 height=tip_size * 2, 3817 axis=(-1, 0, 0), 3818 c=xline_color, 3819 res=12, 3820 ) 3821 else: 3822 cx = shapes.Cone( 3823 (dx, 0, 0), 3824 r=tip_size, 3825 height=tip_size * 2, 3826 axis=(1, 0, 0), 3827 c=xline_color, 3828 res=12, 3829 ) 3830 T = LinearTransform() 3831 T.translate( 3832 [ 3833 0, 3834 zxshift * dy + xshift_along_y * dy, 3835 xyshift * dz + xshift_along_z * dz, 3836 ] 3837 ) 3838 cx.apply_transform(T) 3839 cx.name = "xTipCone" 3840 cones.append(cx) 3841 3842 if ytitle: 3843 if y_inverted: 3844 cy = shapes.Cone( 3845 r=tip_size, 3846 height=tip_size * 2, 3847 axis=(0, -1, 0), 3848 c=yline_color, 3849 res=12, 3850 ) 3851 else: 3852 cy = shapes.Cone( 3853 (0, dy, 0), 3854 r=tip_size, 3855 height=tip_size * 2, 3856 axis=(0, 1, 0), 3857 c=yline_color, 3858 res=12, 3859 ) 3860 T = LinearTransform() 3861 T.translate( 3862 [ 3863 yzshift * dx + yshift_along_x * dx, 3864 0, 3865 xyshift * dz + yshift_along_z * dz, 3866 ] 3867 ) 3868 cy.apply_transform(T) 3869 cy.name = "yTipCone" 3870 cones.append(cy) 3871 3872 if ztitle: 3873 if z_inverted: 3874 cz = shapes.Cone( 3875 r=tip_size, 3876 height=tip_size * 2, 3877 axis=(0, 0, -1), 3878 c=zline_color, 3879 res=12, 3880 ) 3881 else: 3882 cz = shapes.Cone( 3883 (0, 0, dz), 3884 r=tip_size, 3885 height=tip_size * 2, 3886 axis=(0, 0, 1), 3887 c=zline_color, 3888 res=12, 3889 ) 3890 T = LinearTransform() 3891 T.translate( 3892 [ 3893 yzshift * dx + zshift_along_x * dx, 3894 zxshift * dy + zshift_along_y * dy, 3895 0, 3896 ] 3897 ) 3898 cz.apply_transform(T) 3899 cz.name = "zTipCone" 3900 cones.append(cz) 3901 3902 ################################################################# MAJOR ticks 3903 majorticks, minorticks = [], [] 3904 xticks, yticks, zticks = [], [], [] 3905 if show_ticks: 3906 if xtitle: 3907 tick_thickness = xtick_thickness * gscale / 2 3908 tick_length = xtick_length * gscale / 2 3909 for i in range(1, len(xticks_float) - 1): 3910 v1 = (xticks_float[i] - tick_thickness, -tick_length, 0) 3911 v2 = (xticks_float[i] + tick_thickness, tick_length, 0) 3912 xticks.append(shapes.Rectangle(v1, v2)) 3913 if len(xticks) > 1: 3914 xmajticks = merge(xticks).c(xlabel_color) 3915 T = LinearTransform() 3916 T.rotate_x(xaxis_rotation) 3917 T.translate([0, zxshift*dy + xshift_along_y*dy, xyshift*dz + xshift_along_z*dz]) 3918 xmajticks.apply_transform(T) 3919 xmajticks.name = "xMajorTicks" 3920 majorticks.append(xmajticks) 3921 if ytitle: 3922 tick_thickness = ytick_thickness * gscale / 2 3923 tick_length = ytick_length * gscale / 2 3924 for i in range(1, len(yticks_float) - 1): 3925 v1 = (-tick_length, yticks_float[i] - tick_thickness, 0) 3926 v2 = (tick_length, yticks_float[i] + tick_thickness, 0) 3927 yticks.append(shapes.Rectangle(v1, v2)) 3928 if len(yticks) > 1: 3929 ymajticks = merge(yticks).c(ylabel_color) 3930 T = LinearTransform() 3931 T.rotate_y(yaxis_rotation) 3932 T.translate([yzshift*dx + yshift_along_x*dx, 0, xyshift*dz + yshift_along_z*dz]) 3933 ymajticks.apply_transform(T) 3934 ymajticks.name = "yMajorTicks" 3935 majorticks.append(ymajticks) 3936 if ztitle: 3937 tick_thickness = ztick_thickness * gscale / 2 3938 tick_length = ztick_length * gscale / 2.85 3939 for i in range(1, len(zticks_float) - 1): 3940 v1 = (zticks_float[i] - tick_thickness, -tick_length, 0) 3941 v2 = (zticks_float[i] + tick_thickness, tick_length, 0) 3942 zticks.append(shapes.Rectangle(v1, v2)) 3943 if len(zticks) > 1: 3944 zmajticks = merge(zticks).c(zlabel_color) 3945 T = LinearTransform() 3946 T.rotate_y(-90).rotate_z(-45 + zaxis_rotation) 3947 T.translate([yzshift*dx + zshift_along_x*dx, zxshift*dy + zshift_along_y*dy, 0]) 3948 zmajticks.apply_transform(T) 3949 zmajticks.name = "zMajorTicks" 3950 majorticks.append(zmajticks) 3951 3952 ############################################################# MINOR ticks 3953 if xtitle and xminor_ticks and len(xticks) > 1: 3954 tick_thickness = xtick_thickness * gscale / 4 3955 tick_length = xtick_length * gscale / 4 3956 xminor_ticks += 1 3957 ticks = [] 3958 for i in range(1, len(xticks)): 3959 t0, t1 = xticks[i - 1].pos(), xticks[i].pos() 3960 dt = t1 - t0 3961 for j in range(1, xminor_ticks): 3962 mt = dt * (j / xminor_ticks) + t0 3963 v1 = (mt[0] - tick_thickness, -tick_length, 0) 3964 v2 = (mt[0] + tick_thickness, tick_length, 0) 3965 ticks.append(shapes.Rectangle(v1, v2)) 3966 3967 # finish off the fist lower range from start to first tick 3968 t0, t1 = xticks[0].pos(), xticks[1].pos() 3969 dt = t1 - t0 3970 for j in range(1, xminor_ticks): 3971 mt = t0 - dt * (j / xminor_ticks) 3972 if mt[0] < 0: 3973 break 3974 v1 = (mt[0] - tick_thickness, -tick_length, 0) 3975 v2 = (mt[0] + tick_thickness, tick_length, 0) 3976 ticks.append(shapes.Rectangle(v1, v2)) 3977 3978 # finish off the last upper range from last tick to end 3979 t0, t1 = xticks[-2].pos(), xticks[-1].pos() 3980 dt = t1 - t0 3981 for j in range(1, xminor_ticks): 3982 mt = t1 + dt * (j / xminor_ticks) 3983 if mt[0] > dx: 3984 break 3985 v1 = (mt[0] - tick_thickness, -tick_length, 0) 3986 v2 = (mt[0] + tick_thickness, tick_length, 0) 3987 ticks.append(shapes.Rectangle(v1, v2)) 3988 3989 if ticks: 3990 xminticks = merge(ticks).c(xlabel_color) 3991 T = LinearTransform() 3992 T.rotate_x(xaxis_rotation) 3993 T.translate([0, zxshift*dy + xshift_along_y*dy, xyshift*dz + xshift_along_z*dz]) 3994 xminticks.apply_transform(T) 3995 xminticks.name = "xMinorTicks" 3996 minorticks.append(xminticks) 3997 3998 if ytitle and yminor_ticks and len(yticks) > 1: ##### y 3999 tick_thickness = ytick_thickness * gscale / 4 4000 tick_length = ytick_length * gscale / 4 4001 yminor_ticks += 1 4002 ticks = [] 4003 for i in range(1, len(yticks)): 4004 t0, t1 = yticks[i - 1].pos(), yticks[i].pos() 4005 dt = t1 - t0 4006 for j in range(1, yminor_ticks): 4007 mt = dt * (j / yminor_ticks) + t0 4008 v1 = (-tick_length, mt[1] - tick_thickness, 0) 4009 v2 = (tick_length, mt[1] + tick_thickness, 0) 4010 ticks.append(shapes.Rectangle(v1, v2)) 4011 4012 # finish off the fist lower range from start to first tick 4013 t0, t1 = yticks[0].pos(), yticks[1].pos() 4014 dt = t1 - t0 4015 for j in range(1, yminor_ticks): 4016 mt = t0 - dt * (j / yminor_ticks) 4017 if mt[1] < 0: 4018 break 4019 v1 = (-tick_length, mt[1] - tick_thickness, 0) 4020 v2 = (tick_length, mt[1] + tick_thickness, 0) 4021 ticks.append(shapes.Rectangle(v1, v2)) 4022 4023 # finish off the last upper range from last tick to end 4024 t0, t1 = yticks[-2].pos(), yticks[-1].pos() 4025 dt = t1 - t0 4026 for j in range(1, yminor_ticks): 4027 mt = t1 + dt * (j / yminor_ticks) 4028 if mt[1] > dy: 4029 break 4030 v1 = (-tick_length, mt[1] - tick_thickness, 0) 4031 v2 = (tick_length, mt[1] + tick_thickness, 0) 4032 ticks.append(shapes.Rectangle(v1, v2)) 4033 4034 if ticks: 4035 yminticks = merge(ticks).c(ylabel_color) 4036 T = LinearTransform() 4037 T.rotate_y(yaxis_rotation) 4038 T.translate([yzshift*dx + yshift_along_x*dx, 0, xyshift*dz + yshift_along_z*dz]) 4039 yminticks.apply_transform(T) 4040 yminticks.name = "yMinorTicks" 4041 minorticks.append(yminticks) 4042 4043 if ztitle and zminor_ticks and len(zticks) > 1: ##### z 4044 tick_thickness = ztick_thickness * gscale / 4 4045 tick_length = ztick_length * gscale / 5 4046 zminor_ticks += 1 4047 ticks = [] 4048 for i in range(1, len(zticks)): 4049 t0, t1 = zticks[i - 1].pos(), zticks[i].pos() 4050 dt = t1 - t0 4051 for j in range(1, zminor_ticks): 4052 mt = dt * (j / zminor_ticks) + t0 4053 v1 = (mt[0] - tick_thickness, -tick_length, 0) 4054 v2 = (mt[0] + tick_thickness, tick_length, 0) 4055 ticks.append(shapes.Rectangle(v1, v2)) 4056 4057 # finish off the fist lower range from start to first tick 4058 t0, t1 = zticks[0].pos(), zticks[1].pos() 4059 dt = t1 - t0 4060 for j in range(1, zminor_ticks): 4061 mt = t0 - dt * (j / zminor_ticks) 4062 if mt[0] < 0: 4063 break 4064 v1 = (mt[0] - tick_thickness, -tick_length, 0) 4065 v2 = (mt[0] + tick_thickness, tick_length, 0) 4066 ticks.append(shapes.Rectangle(v1, v2)) 4067 4068 # finish off the last upper range from last tick to end 4069 t0, t1 = zticks[-2].pos(), zticks[-1].pos() 4070 dt = t1 - t0 4071 for j in range(1, zminor_ticks): 4072 mt = t1 + dt * (j / zminor_ticks) 4073 if mt[0] > dz: 4074 break 4075 v1 = (mt[0] - tick_thickness, -tick_length, 0) 4076 v2 = (mt[0] + tick_thickness, tick_length, 0) 4077 ticks.append(shapes.Rectangle(v1, v2)) 4078 4079 if ticks: 4080 zminticks = merge(ticks).c(zlabel_color) 4081 T = LinearTransform() 4082 T.rotate_y(-90).rotate_z(-45 + zaxis_rotation) 4083 T.translate([yzshift*dx + zshift_along_x*dx, zxshift*dy + zshift_along_y*dy, 0]) 4084 zminticks.apply_transform(T) 4085 zminticks.name = "zMinorTicks" 4086 minorticks.append(zminticks) 4087 4088 ################################################ axes NUMERIC text labels 4089 labels = [] 4090 xlab, ylab, zlab = None, None, None 4091 4092 if xlabel_size and xtitle: 4093 4094 xRot, yRot, zRot = 0, 0, 0 4095 if utils.is_sequence(xlabel_rotation): # unpck 3 rotations 4096 zRot, xRot, yRot = xlabel_rotation 4097 else: 4098 zRot = xlabel_rotation 4099 if zRot < 0: # deal with negative angles 4100 zRot += 360 4101 4102 jus = "center-top" 4103 if zRot: 4104 if zRot > 24: jus = "top-right" 4105 if zRot > 67: jus = "center-right" 4106 if zRot > 112: jus = "right-bottom" 4107 if zRot > 157: jus = "center-bottom" 4108 if zRot > 202: jus = "bottom-left" 4109 if zRot > 247: jus = "center-left" 4110 if zRot > 292: jus = "top-left" 4111 if zRot > 337: jus = "top-center" 4112 if xlabel_justify is not None: 4113 jus = xlabel_justify 4114 4115 for i in range(1, len(xticks_str)): 4116 t = xticks_str[i] 4117 if not t: 4118 continue 4119 if utils.is_sequence(xlabel_offset): 4120 xoffs, yoffs, zoffs = xlabel_offset 4121 else: 4122 xoffs, yoffs, zoffs = 0, xlabel_offset, 0 4123 4124 xlab = shapes.Text3D( 4125 t, s=xlabel_size * text_scale * gscale, font=label_font, justify=jus 4126 ) 4127 tb = xlab.ybounds() # must be ybounds: height of char 4128 4129 v = (xticks_float[i], 0, 0) 4130 offs = -np.array([xoffs, yoffs, zoffs]) * (tb[1] - tb[0]) 4131 4132 T = LinearTransform() 4133 T.rotate_x(xaxis_rotation).rotate_y(yRot).rotate_x(xRot).rotate_z(zRot) 4134 T.translate(v + offs) 4135 T.translate([0, zxshift*dy + xshift_along_y*dy, xyshift*dz + xshift_along_z*dz]) 4136 xlab.apply_transform(T) 4137 4138 xlab.use_bounds(x_use_bounds) 4139 4140 xlab.c(xlabel_color) 4141 if xlabel_backface_color is None: 4142 bfc = 1 - np.array(get_color(xlabel_color)) 4143 xlab.backcolor(bfc) 4144 xlab.name = f"xNumericLabel {i}" 4145 labels.append(xlab) 4146 4147 if ylabel_size and ytitle: 4148 4149 xRot, yRot, zRot = 0, 0, 0 4150 if utils.is_sequence(ylabel_rotation): # unpck 3 rotations 4151 zRot, yRot, xRot = ylabel_rotation 4152 else: 4153 zRot = ylabel_rotation 4154 if zRot < 0: 4155 zRot += 360 # deal with negative angles 4156 4157 jus = "center-right" 4158 if zRot: 4159 if zRot > 24: jus = "bottom-right" 4160 if zRot > 67: jus = "center-bottom" 4161 if zRot > 112: jus = "left-bottom" 4162 if zRot > 157: jus = "center-left" 4163 if zRot > 202: jus = "top-left" 4164 if zRot > 247: jus = "center-top" 4165 if zRot > 292: jus = "top-right" 4166 if zRot > 337: jus = "right-center" 4167 if ylabel_justify is not None: 4168 jus = ylabel_justify 4169 4170 for i in range(1, len(yticks_str)): 4171 t = yticks_str[i] 4172 if not t: 4173 continue 4174 if utils.is_sequence(ylabel_offset): 4175 xoffs, yoffs, zoffs = ylabel_offset 4176 else: 4177 xoffs, yoffs, zoffs = ylabel_offset, 0, 0 4178 ylab = shapes.Text3D( 4179 t, s=ylabel_size * text_scale * gscale, font=label_font, justify=jus 4180 ) 4181 tb = ylab.ybounds() # must be ybounds: height of char 4182 v = (0, yticks_float[i], 0) 4183 offs = -np.array([xoffs, yoffs, zoffs]) * (tb[1] - tb[0]) 4184 4185 T = LinearTransform() 4186 T.rotate_y(yaxis_rotation).rotate_x(xRot).rotate_y(yRot).rotate_z(zRot) 4187 T.translate(v + offs) 4188 T.translate([yzshift*dx + yshift_along_x*dx, 0, xyshift*dz + yshift_along_z*dz]) 4189 ylab.apply_transform(T) 4190 4191 ylab.use_bounds(y_use_bounds) 4192 4193 ylab.c(ylabel_color) 4194 if ylabel_backface_color is None: 4195 bfc = 1 - np.array(get_color(ylabel_color)) 4196 ylab.backcolor(bfc) 4197 ylab.name = f"yNumericLabel {i}" 4198 labels.append(ylab) 4199 4200 if zlabel_size and ztitle: 4201 4202 xRot, yRot, zRot = 0, 0, 0 4203 if utils.is_sequence(zlabel_rotation): # unpck 3 rotations 4204 xRot, yRot, zRot = zlabel_rotation 4205 else: 4206 xRot = zlabel_rotation 4207 if xRot < 0: xRot += 360 # deal with negative angles 4208 4209 jus = "center-right" 4210 if xRot: 4211 if xRot > 24: jus = "bottom-right" 4212 if xRot > 67: jus = "center-bottom" 4213 if xRot > 112: jus = "left-bottom" 4214 if xRot > 157: jus = "center-left" 4215 if xRot > 202: jus = "top-left" 4216 if xRot > 247: jus = "center-top" 4217 if xRot > 292: jus = "top-right" 4218 if xRot > 337: jus = "right-center" 4219 if zlabel_justify is not None: 4220 jus = zlabel_justify 4221 4222 for i in range(1, len(zticks_str)): 4223 t = zticks_str[i] 4224 if not t: 4225 continue 4226 if utils.is_sequence(zlabel_offset): 4227 xoffs, yoffs, zoffs = zlabel_offset 4228 else: 4229 xoffs, yoffs, zoffs = zlabel_offset, zlabel_offset, 0 4230 zlab = shapes.Text3D(t, s=zlabel_size*text_scale*gscale, font=label_font, justify=jus) 4231 tb = zlab.ybounds() # must be ybounds: height of char 4232 4233 v = (0, 0, zticks_float[i]) 4234 offs = -np.array([xoffs, yoffs, zoffs]) * (tb[1] - tb[0]) / 1.5 4235 angle = np.arctan2(dy, dx) * 57.3 4236 4237 T = LinearTransform() 4238 T.rotate_x(90 + zRot).rotate_y(-xRot).rotate_z(angle + yRot + zaxis_rotation) 4239 T.translate(v + offs) 4240 T.translate([yzshift*dx + zshift_along_x*dx, zxshift*dy + zshift_along_y*dy, 0]) 4241 zlab.apply_transform(T) 4242 4243 zlab.use_bounds(z_use_bounds) 4244 4245 zlab.c(zlabel_color) 4246 if zlabel_backface_color is None: 4247 bfc = 1 - np.array(get_color(zlabel_color)) 4248 zlab.backcolor(bfc) 4249 zlab.name = f"zNumericLabel {i}" 4250 labels.append(zlab) 4251 4252 ################################################ axes titles 4253 titles = [] 4254 4255 if xtitle: 4256 xRot, yRot, zRot = 0, 0, 0 4257 if utils.is_sequence(xtitle_rotation): # unpack 3 rotations 4258 zRot, xRot, yRot = xtitle_rotation 4259 else: 4260 zRot = xtitle_rotation 4261 if zRot < 0: # deal with negative angles 4262 zRot += 360 4263 4264 if utils.is_sequence(xtitle_offset): 4265 xoffs, yoffs, zoffs = xtitle_offset 4266 else: 4267 xoffs, yoffs, zoffs = 0, xtitle_offset, 0 4268 4269 if xtitle_justify is not None: 4270 jus = xtitle_justify 4271 else: 4272 # find best justfication for given rotation(s) 4273 jus = "right-top" 4274 if zRot: 4275 if zRot > 24: jus = "center-right" 4276 if zRot > 67: jus = "right-bottom" 4277 if zRot > 157: jus = "bottom-left" 4278 if zRot > 202: jus = "center-left" 4279 if zRot > 247: jus = "top-left" 4280 if zRot > 337: jus = "top-right" 4281 4282 xt = shapes.Text3D( 4283 xtitle, 4284 s=xtitle_size * text_scale * gscale, 4285 font=title_font, 4286 c=xtitle_color, 4287 justify=jus, 4288 depth=title_depth, 4289 italic=xtitle_italic, 4290 ) 4291 if xtitle_backface_color is None: 4292 xtitle_backface_color = 1 - np.array(get_color(xtitle_color)) 4293 xt.backcolor(xtitle_backface_color) 4294 4295 shift = 0 4296 if xlab: # xlab is the last created numeric text label.. 4297 lt0, lt1 = xlab.bounds()[2:4] 4298 shift = lt1 - lt0 4299 4300 T = LinearTransform() 4301 T.rotate_x(xRot).rotate_y(yRot).rotate_z(zRot) 4302 T.set_position( 4303 [(xoffs + xtitle_position) * dx, 4304 -(yoffs + xtick_length / 2) * dy - shift, 4305 zoffs * dz] 4306 ) 4307 T.rotate_x(xaxis_rotation) 4308 T.translate([0, xshift_along_y * dy, xyshift * dz + xshift_along_z * dz]) 4309 xt.apply_transform(T) 4310 4311 xt.use_bounds(x_use_bounds) 4312 if xtitle == " ": 4313 xt.use_bounds(False) 4314 xt.name = "xtitle" 4315 titles.append(xt) 4316 if xtit