vedo.addons

Create additional objects like axes, legends, lights, etc.

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