vedo.addons

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

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