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