vedo.addons

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

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