vedo.addons

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

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