vedo.plotter

This module defines the main class Plotter to manage objects and 3D rendering.

   1#!/usr/bin/env python3
   2# -*- coding: utf-8 -*-
   3import os.path
   4import sys
   5import time
   6from typing import MutableSequence, Callable, Any, Union
   7from typing_extensions import Self
   8import numpy as np
   9
  10import vedo.vtkclasses as vtki  # a wrapper for lazy imports
  11
  12import vedo
  13from vedo import transformations
  14from vedo import utils
  15from vedo import backends
  16from vedo import addons
  17
  18
  19__docformat__ = "google"
  20
  21__doc__ = """
  22This module defines the main class Plotter to manage objects and 3D rendering.
  23
  24![](https://vedo.embl.es/images/basic/multirenderers.png)
  25"""
  26
  27__all__ = ["Plotter", "show", "close"]
  28
  29########################################################################################
  30class Event:
  31    """
  32    This class holds the info from an event in the window, works as dictionary too.
  33    """
  34
  35    __slots__ = [
  36        "name",
  37        "title",
  38        "id",
  39        "timerid",
  40        "time",
  41        "priority",
  42        "at",
  43        "object",
  44        "actor",
  45        "picked3d",
  46        "keypress",
  47        "picked2d",
  48        "delta2d",
  49        "angle2d",
  50        "speed2d",
  51        "delta3d",
  52        "speed3d",
  53        "isPoints",
  54        "isMesh",
  55        "isAssembly",
  56        "isVolume",
  57        "isImage",
  58        "isActor2D",
  59    ]
  60
  61    def __init__(self):
  62        self.name = "event"
  63        self.title = ""
  64        self.id = 0
  65        self.timerid = 0
  66        self.time = 0
  67        self.priority = 0
  68        self.at = 0
  69        self.object = None
  70        self.actor = None
  71        self.picked3d = ()
  72        self.keypress = ""
  73        self.picked2d = ()
  74        self.delta2d = ()
  75        self.angle2d = 0
  76        self.speed2d = ()
  77        self.delta3d = ()
  78        self.speed3d = 0
  79        self.isPoints = False
  80        self.isMesh = False
  81        self.isAssembly = False
  82        self.isVolume = False
  83        self.isImage = False
  84        self.isActor2D = False
  85
  86    def __getitem__(self, key):
  87        return getattr(self, key)
  88
  89    def __setitem__(self, key, value):
  90        setattr(self, key, value)
  91
  92    def __str__(self):
  93        module = self.__class__.__module__
  94        name = self.__class__.__name__
  95        out = vedo.printc(
  96            f"{module}.{name} at ({hex(id(self))})".ljust(75),
  97            bold=True, invert=True, return_string=True,
  98        )
  99        out += "\x1b[0m"
 100        for n in self.__slots__:
 101            if n == "actor":
 102                continue
 103            out += f"{n}".ljust(11) + ": "
 104            val = str(self[n]).replace("\n", "")[:65].rstrip()
 105            if val == "True":
 106                out += "\x1b[32;1m"
 107            elif val == "False":
 108                out += "\x1b[31;1m"
 109            out += val + "\x1b[0m\n"
 110        return out.rstrip()
 111
 112    def keys(self):
 113        return self.__slots__
 114
 115
 116##############################################################################################
 117def show(
 118    *objects,
 119    at=None,
 120    shape=(1, 1),
 121    N=None,
 122    pos=(0, 0),
 123    size="auto",
 124    screensize="auto",
 125    title="vedo",
 126    bg="white",
 127    bg2=None,
 128    axes=None,
 129    interactive=None,
 130    offscreen=False,
 131    sharecam=True,
 132    resetcam=True,
 133    zoom=None,
 134    viewup="",
 135    azimuth=0.0,
 136    elevation=0.0,
 137    roll=0.0,
 138    camera=None,
 139    mode=None,
 140    screenshot="",
 141    new=False,
 142) -> Union[Self, None]:
 143    """
 144    Create on the fly an instance of class Plotter and show the object(s) provided.
 145
 146    Arguments:
 147        at : (int)
 148            number of the renderer to plot to, in case of more than one exists
 149        shape : (list, str)
 150            Number of sub-render windows inside of the main window. E.g.:
 151            specify two across with shape=(2,1) and a two by two grid
 152            with shape=(2, 2). By default there is only one renderer.
 153
 154            Can also accept a shape as string descriptor. E.g.:
 155            - shape="3|1" means 3 plots on the left and 1 on the right,
 156            - shape="4/2" means 4 plots on top of 2 at bottom.
 157        N : (int)
 158            number of desired sub-render windows arranged automatically in a grid
 159        pos : (list)
 160            position coordinates of the top-left corner of the rendering window
 161            on the screen
 162        size : (list)
 163            size of the rendering window
 164        screensize : (list)
 165            physical size of the monitor screen
 166        title : (str)
 167            window title
 168        bg : (color)
 169            background color or specify jpg image file name with path
 170        bg2 : (color)
 171            background color of a gradient towards the top
 172        axes : (int)
 173            set the type of axes to be shown:
 174            - 0,  no axes
 175            - 1,  draw three gray grid walls
 176            - 2,  show cartesian axes from (0,0,0)
 177            - 3,  show positive range of cartesian axes from (0,0,0)
 178            - 4,  show a triad at bottom left
 179            - 5,  show a cube at bottom left
 180            - 6,  mark the corners of the bounding box
 181            - 7,  draw a 3D ruler at each side of the cartesian axes
 182            - 8,  show the `vtkCubeAxesActor` object
 183            - 9,  show the bounding box outLine
 184            - 10, show three circles representing the maximum bounding box
 185            - 11, show a large grid on the x-y plane
 186            - 12, show polar axes
 187            - 13, draw a simple ruler at the bottom of the window
 188            - 14: draw a `CameraOrientationWidget`
 189
 190            Axis type-1 can be fully customized by passing a dictionary.
 191            Check `vedo.addons.Axes()` for the full list of options.
 192        azimuth/elevation/roll : (float)
 193            move camera accordingly the specified value
 194        viewup : (str, list)
 195            either `['x', 'y', 'z']` or a vector to set vertical direction
 196        resetcam : (bool)
 197            re-adjust camera position to fit objects
 198        camera : (dict, vtkCamera)
 199            camera parameters can further be specified with a dictionary
 200            assigned to the `camera` keyword (E.g. `show(camera={'pos':(1,2,3), 'thickness':1000,})`):
 201            - **pos** (list),  the position of the camera in world coordinates
 202            - **focal_point** (list), the focal point of the camera in world coordinates
 203            - **viewup** (list), the view up direction for the camera
 204            - **distance** (float), set the focal point to the specified distance from the camera position.
 205            - **clipping_range** (float), distance of the near and far clipping planes along the direction of projection.
 206            - **parallel_scale** (float),
 207            scaling used for a parallel projection, i.e. the height of the viewport
 208            in world-coordinate distances. The default is 1. Note that the "scale" parameter works as
 209            an "inverse scale", larger numbers produce smaller images.
 210            This method has no effect in perspective projection mode.
 211            - **thickness** (float),
 212            set the distance between clipping planes. This method adjusts the far clipping
 213            plane to be set a distance 'thickness' beyond the near clipping plane.
 214            - **view_angle** (float),
 215            the camera view angle, which is the angular height of the camera view
 216            measured in degrees. The default angle is 30 degrees.
 217            This method has no effect in parallel projection mode.
 218            The formula for setting the angle up for perfect perspective viewing is:
 219            angle = 2*atan((h/2)/d) where h is the height of the RenderWindow
 220            (measured by holding a ruler up to your screen) and d is the distance
 221            from your eyes to the screen.
 222        interactive : (bool)
 223            pause and interact with window (True) or continue execution (False)
 224        rate : (float)
 225            maximum rate of `show()` in Hertz
 226        mode : (int, str)
 227            set the type of interaction:
 228            - 0 = TrackballCamera [default]
 229            - 1 = TrackballActor
 230            - 2 = JoystickCamera
 231            - 3 = JoystickActor
 232            - 4 = Flight
 233            - 5 = RubberBand2D
 234            - 6 = RubberBand3D
 235            - 7 = RubberBandZoom
 236            - 8 = Terrain
 237            - 9 = Unicam
 238            - 10 = Image
 239        new : (bool)
 240            if set to `True`, a call to show will instantiate
 241            a new Plotter object (a new window) instead of reusing the first created.
 242            If new is `True`, but the existing plotter was instantiated with a different
 243            argument for `offscreen`, `new` is ignored and a new Plotter is created anyway.
 244    """
 245    if len(objects) == 0:
 246        objects = None
 247    elif len(objects) == 1:
 248        objects = objects[0]
 249    else:
 250        objects = utils.flatten(objects)
 251
 252    # If a plotter instance is already present, check if the offscreen argument
 253    # is the same as the one requested by the user. If not, create a new
 254    # plotter instance (see https://github.com/marcomusy/vedo/issues/1026)
 255    if vedo.plotter_instance and vedo.plotter_instance.offscreen != offscreen:
 256        new = True
 257
 258    if vedo.plotter_instance and not new:  # Plotter exists
 259        plt = vedo.plotter_instance
 260
 261    else:  # Plotter must be created
 262
 263        if utils.is_sequence(at):  # user passed a sequence for "at"
 264
 265            if not utils.is_sequence(objects):
 266                vedo.logger.error("in show() input must be a list.")
 267                raise RuntimeError()
 268            if len(at) != len(objects):
 269                vedo.logger.error("in show() lists 'input' and 'at' must have equal lengths")
 270                raise RuntimeError()
 271            if shape == (1, 1) and N is None:
 272                N = max(at) + 1
 273
 274        elif at is None and (N or shape != (1, 1)):
 275
 276            if not utils.is_sequence(objects):
 277                e = "in show(), N or shape is set, but input is not a sequence\n"
 278                e += "              you may need to specify e.g. at=0"
 279                vedo.logger.error(e)
 280                raise RuntimeError()
 281            at = list(range(len(objects)))
 282
 283        plt = Plotter(
 284            shape=shape,
 285            N=N,
 286            pos=pos,
 287            size=size,
 288            screensize=screensize,
 289            title=title,
 290            axes=axes,
 291            sharecam=sharecam,
 292            resetcam=resetcam,
 293            interactive=interactive,
 294            offscreen=offscreen,
 295            bg=bg,
 296            bg2=bg2,
 297        )
 298
 299    if vedo.settings.dry_run_mode >= 2:
 300        return plt
 301
 302    # use _plt_to_return because plt.show() can return a k3d plot
 303    _plt_to_return = None
 304
 305    if utils.is_sequence(at):
 306
 307        for i, act in enumerate(objects):
 308            _plt_to_return = plt.show(
 309                act,
 310                at=i,
 311                zoom=zoom,
 312                resetcam=resetcam,
 313                viewup=viewup,
 314                azimuth=azimuth,
 315                elevation=elevation,
 316                roll=roll,
 317                camera=camera,
 318                interactive=False,
 319                mode=mode,
 320                screenshot=screenshot,
 321                bg=bg,
 322                bg2=bg2,
 323                axes=axes,
 324            )
 325
 326        if (
 327            interactive
 328            or len(at) == N
 329            or (isinstance(shape[0], int) and len(at) == shape[0] * shape[1])
 330        ):
 331            # note that shape can be a string
 332            if plt.interactor and not offscreen and (interactive is None or interactive):
 333                plt.interactor.Start()
 334                if plt._must_close_now:
 335                    plt.interactor.GetRenderWindow().Finalize()
 336                    plt.interactor.TerminateApp()
 337                    plt.interactor = None
 338                    plt.window = None
 339                    plt.renderer = None
 340                    plt.renderers = []
 341                    plt.camera = None
 342                    
 343    else:
 344
 345        _plt_to_return = plt.show(
 346            objects,
 347            at=at,
 348            zoom=zoom,
 349            resetcam=resetcam,
 350            viewup=viewup,
 351            azimuth=azimuth,
 352            elevation=elevation,
 353            roll=roll,
 354            camera=camera,
 355            interactive=interactive,
 356            mode=mode,
 357            screenshot=screenshot,
 358            bg=bg,
 359            bg2=bg2,
 360            axes=axes,
 361        )
 362
 363    return _plt_to_return
 364
 365
 366def close() -> None:
 367    """Close the last created Plotter instance if it exists."""
 368    if not vedo.plotter_instance:
 369        return
 370    vedo.plotter_instance.close()
 371    return
 372
 373
 374########################################################################
 375class Plotter:
 376    """Main class to manage objects."""
 377
 378    def __init__(
 379        self,
 380        shape=(1, 1),
 381        N=None,
 382        pos=(0, 0),
 383        size="auto",
 384        screensize="auto",
 385        title="vedo",
 386        bg="white",
 387        bg2=None,
 388        axes=None,
 389        sharecam=True,
 390        resetcam=True,
 391        interactive=None,
 392        offscreen=False,
 393        qt_widget=None,
 394        wx_widget=None,
 395    ):
 396        """
 397        Arguments:
 398            shape : (str, list)
 399                shape of the grid of renderers in format (rows, columns). Ignored if N is specified.
 400            N : (int)
 401                number of desired renderers arranged in a grid automatically.
 402            pos : (list)
 403                (x,y) position in pixels of top-left corner of the rendering window on the screen
 404            size : (str, list)
 405                size of the rendering window. If 'auto', guess it based on screensize.
 406            screensize : (list)
 407                physical size of the monitor screen in pixels
 408            bg : (color, str)
 409                background color or specify jpg image file name with path
 410            bg2 : (color)
 411                background color of a gradient towards the top
 412            title : (str)
 413                window title
 414
 415            axes : (int)
 416
 417                Note that Axes type-1 can be fully customized by passing a dictionary `axes=dict()`.
 418                Check out `vedo.addons.Axes()` for the available options.
 419
 420                    - 0,  no axes
 421                    - 1,  draw three gray grid walls
 422                    - 2,  show cartesian axes from (0,0,0)
 423                    - 3,  show positive range of cartesian axes from (0,0,0)
 424                    - 4,  show a triad at bottom left
 425                    - 5,  show a cube at bottom left
 426                    - 6,  mark the corners of the bounding box
 427                    - 7,  draw a 3D ruler at each side of the cartesian axes
 428                    - 8,  show the VTK CubeAxesActor object
 429                    - 9,  show the bounding box outLine
 430                    - 10, show three circles representing the maximum bounding box
 431                    - 11, show a large grid on the x-y plane (use with zoom=8)
 432                    - 12, show polar axes
 433                    - 13, draw a simple ruler at the bottom of the window
 434                    - 14: draw a camera orientation widget
 435
 436            sharecam : (bool)
 437                if False each renderer will have an independent camera
 438            interactive : (bool)
 439                if True will stop after show() to allow interaction with the 3d scene
 440            offscreen : (bool)
 441                if True will not show the rendering window
 442            qt_widget : (QVTKRenderWindowInteractor)
 443                render in a Qt-Widget using an QVTKRenderWindowInteractor.
 444                See examples `qt_windows[1,2,3].py` and `qt_cutter.py`.
 445        """
 446        vedo.plotter_instance = self
 447
 448        if interactive is None:
 449            interactive = bool(N in (0, 1, None) and shape == (1, 1))
 450        self._interactive = interactive
 451        # print("interactive", interactive, N, shape)
 452
 453        self.objects = []           # list of objects to be shown
 454        self.clicked_object = None  # holds the object that has been clicked
 455        self.clicked_actor = None   # holds the actor that has been clicked
 456
 457        self.shape = shape   # nr. of subwindows in grid
 458        self.axes = axes     # show axes type nr.
 459        self.title = title   # window title
 460        self.size = size     # window size
 461        self.backgrcol = bg  # used also by backend notebooks
 462
 463        self.offscreen= offscreen
 464        self.resetcam = resetcam
 465        self.sharecam = sharecam  # share the same camera if multiple renderers
 466        self.pos      = pos       # used by vedo.file_io
 467
 468        self.picker   = None  # hold the vtkPicker object
 469        self.picked2d = None  # 2d coords of a clicked point on the rendering window
 470        self.picked3d = None  # 3d coords of a clicked point on an actor
 471
 472        self.qt_widget = qt_widget  # QVTKRenderWindowInteractor
 473        self.wx_widget = wx_widget  # wxVTKRenderWindowInteractor
 474        self.interactor = None
 475        self.window = None
 476        self.renderer = None
 477        self.renderers = []  # list of renderers
 478
 479        # mostly internal stuff:
 480        self.hover_legends = []
 481        self.justremoved = None
 482        self.axes_instances = []
 483        self.clock = 0
 484        self.sliders = []
 485        self.buttons = []
 486        self.widgets = []
 487        self.cutter_widget = None
 488        self.hint_widget = None
 489        self.background_renderer = None
 490        self.last_event = None
 491        self.skybox = None
 492        self._icol = 0
 493        self._clockt0 = time.time()
 494        self._extralight = None
 495        self._cocoa_initialized = False
 496        self._cocoa_process_events = True  # make one call in show()
 497        self._must_close_now = False
 498
 499        #####################################################################
 500        if vedo.settings.default_backend == "2d":
 501            self.offscreen = True
 502            if self.size == "auto":
 503                self.size = (800, 600)
 504
 505        elif vedo.settings.default_backend == "k3d":
 506            if self.size == "auto":
 507                self.size = (1000, 1000)
 508            ####################################
 509            return  ############################
 510            ####################################
 511
 512        #############################################################
 513        if vedo.settings.default_backend in ["vtk", "2d", "trame"]:
 514
 515            if screensize == "auto":
 516                screensize = (2160, 1440)  # TODO: get actual screen size
 517
 518            # build the rendering window:
 519            self.window = vtki.vtkRenderWindow()
 520
 521            self.window.GlobalWarningDisplayOff()
 522
 523            if self.title == "vedo":  # check if dev version
 524                if "dev" in vedo.__version__:
 525                    self.title = f"vedo ({vedo.__version__})"
 526            self.window.SetWindowName(self.title)
 527
 528            # more vedo.settings
 529            if vedo.settings.use_depth_peeling:
 530                self.window.SetAlphaBitPlanes(vedo.settings.alpha_bit_planes)
 531            self.window.SetMultiSamples(vedo.settings.multi_samples)
 532
 533            self.window.SetPolygonSmoothing(vedo.settings.polygon_smoothing)
 534            self.window.SetLineSmoothing(vedo.settings.line_smoothing)
 535            self.window.SetPointSmoothing(vedo.settings.point_smoothing)
 536
 537        #############################################################
 538        if N:  # N = number of renderers. Find out the best
 539
 540            if shape != (1, 1):  # arrangement based on minimum nr. of empty renderers
 541                vedo.logger.warning("having set N, shape is ignored.")
 542
 543            x, y = screensize
 544            nx = int(np.sqrt(int(N * y / x) + 1))
 545            ny = int(np.sqrt(int(N * x / y) + 1))
 546            lm = [
 547                (nx, ny),
 548                (nx, ny + 1),
 549                (nx - 1, ny),
 550                (nx + 1, ny),
 551                (nx, ny - 1),
 552                (nx - 1, ny + 1),
 553                (nx + 1, ny - 1),
 554                (nx + 1, ny + 1),
 555                (nx - 1, ny - 1),
 556            ]
 557            ind, minl = 0, 1000
 558            for i, m in enumerate(lm):
 559                l = m[0] * m[1]
 560                if N <= l < minl:
 561                    ind = i
 562                    minl = l
 563            shape = lm[ind]
 564
 565        ##################################################
 566        if isinstance(shape, str):
 567
 568            if "|" in shape:
 569                if self.size == "auto":
 570                    self.size = (800, 1200)
 571                n = int(shape.split("|")[0])
 572                m = int(shape.split("|")[1])
 573                rangen = reversed(range(n))
 574                rangem = reversed(range(m))
 575            else:
 576                if self.size == "auto":
 577                    self.size = (1200, 800)
 578                m = int(shape.split("/")[0])
 579                n = int(shape.split("/")[1])
 580                rangen = range(n)
 581                rangem = range(m)
 582
 583            if n >= m:
 584                xsplit = m / (n + m)
 585            else:
 586                xsplit = 1 - n / (n + m)
 587            if vedo.settings.window_splitting_position:
 588                xsplit = vedo.settings.window_splitting_position
 589
 590            for i in rangen:
 591                arenderer = vtki.vtkRenderer()
 592                if "|" in shape:
 593                    arenderer.SetViewport(0, i / n, xsplit, (i + 1) / n)
 594                else:
 595                    arenderer.SetViewport(i / n, 0, (i + 1) / n, xsplit)
 596                self.renderers.append(arenderer)
 597
 598            for i in rangem:
 599                arenderer = vtki.vtkRenderer()
 600
 601                if "|" in shape:
 602                    arenderer.SetViewport(xsplit, i / m, 1, (i + 1) / m)
 603                else:
 604                    arenderer.SetViewport(i / m, xsplit, (i + 1) / m, 1)
 605                self.renderers.append(arenderer)
 606
 607            for r in self.renderers:
 608                r.SetLightFollowCamera(vedo.settings.light_follows_camera)
 609
 610                r.SetUseDepthPeeling(vedo.settings.use_depth_peeling)
 611                # r.SetUseDepthPeelingForVolumes(vedo.settings.use_depth_peeling)
 612                if vedo.settings.use_depth_peeling:
 613                    r.SetMaximumNumberOfPeels(vedo.settings.max_number_of_peels)
 614                    r.SetOcclusionRatio(vedo.settings.occlusion_ratio)
 615                r.SetUseFXAA(vedo.settings.use_fxaa)
 616                r.SetPreserveDepthBuffer(vedo.settings.preserve_depth_buffer)
 617
 618                r.SetBackground(vedo.get_color(self.backgrcol))
 619
 620                self.axes_instances.append(None)
 621
 622            self.shape = (n + m,)
 623
 624        elif utils.is_sequence(shape) and isinstance(shape[0], dict):
 625            # passing a sequence of dicts for renderers specifications
 626
 627            if self.size == "auto":
 628                self.size = (1000, 800)
 629
 630            for rd in shape:
 631                x0, y0 = rd["bottomleft"]
 632                x1, y1 = rd["topright"]
 633                bg_ = rd.pop("bg", "white")
 634                bg2_ = rd.pop("bg2", None)
 635
 636                arenderer = vtki.vtkRenderer()
 637                arenderer.SetLightFollowCamera(vedo.settings.light_follows_camera)
 638
 639                arenderer.SetUseDepthPeeling(vedo.settings.use_depth_peeling)
 640                # arenderer.SetUseDepthPeelingForVolumes(vedo.settings.use_depth_peeling)
 641                if vedo.settings.use_depth_peeling:
 642                    arenderer.SetMaximumNumberOfPeels(vedo.settings.max_number_of_peels)
 643                    arenderer.SetOcclusionRatio(vedo.settings.occlusion_ratio)
 644                arenderer.SetUseFXAA(vedo.settings.use_fxaa)
 645                arenderer.SetPreserveDepthBuffer(vedo.settings.preserve_depth_buffer)
 646
 647                arenderer.SetViewport(x0, y0, x1, y1)
 648                arenderer.SetBackground(vedo.get_color(bg_))
 649                if bg2_:
 650                    arenderer.GradientBackgroundOn()
 651                    arenderer.SetBackground2(vedo.get_color(bg2_))
 652
 653                self.renderers.append(arenderer)
 654                self.axes_instances.append(None)
 655
 656            self.shape = (len(shape),)
 657
 658        else:
 659
 660            if isinstance(self.size, str) and self.size == "auto":
 661                # figure out a reasonable window size
 662                f = 1.5
 663                x, y = screensize
 664                xs = y / f * shape[1]  # because y<x
 665                ys = y / f * shape[0]
 666                if xs > x / f:  # shrink
 667                    xs = x / f
 668                    ys = xs / shape[1] * shape[0]
 669                if ys > y / f:
 670                    ys = y / f
 671                    xs = ys / shape[0] * shape[1]
 672                self.size = (int(xs), int(ys))
 673                if shape == (1, 1):
 674                    self.size = (int(y / f), int(y / f))  # because y<x
 675            else:
 676                self.size = (self.size[0], self.size[1])
 677
 678            try:
 679                image_actor = None
 680                bgname = str(self.backgrcol).lower()
 681                if ".jpg" in bgname or ".jpeg" in bgname or ".png" in bgname:
 682                    self.window.SetNumberOfLayers(2)
 683                    self.background_renderer = vtki.vtkRenderer()
 684                    self.background_renderer.SetLayer(0)
 685                    self.background_renderer.InteractiveOff()
 686                    self.background_renderer.SetBackground(vedo.get_color(bg2))
 687                    image_actor = vedo.Image(self.backgrcol).actor
 688                    self.window.AddRenderer(self.background_renderer)
 689                    self.background_renderer.AddActor(image_actor)
 690            except AttributeError:
 691                pass
 692
 693            for i in reversed(range(shape[0])):
 694                for j in range(shape[1]):
 695                    arenderer = vtki.vtkRenderer()
 696                    arenderer.SetLightFollowCamera(vedo.settings.light_follows_camera)
 697                    arenderer.SetTwoSidedLighting(vedo.settings.two_sided_lighting)
 698
 699                    arenderer.SetUseDepthPeeling(vedo.settings.use_depth_peeling)
 700                    # arenderer.SetUseDepthPeelingForVolumes(vedo.settings.use_depth_peeling)
 701                    if vedo.settings.use_depth_peeling:
 702                        arenderer.SetMaximumNumberOfPeels(vedo.settings.max_number_of_peels)
 703                        arenderer.SetOcclusionRatio(vedo.settings.occlusion_ratio)
 704                    arenderer.SetUseFXAA(vedo.settings.use_fxaa)
 705                    arenderer.SetPreserveDepthBuffer(vedo.settings.preserve_depth_buffer)
 706
 707                    if image_actor:
 708                        arenderer.SetLayer(1)
 709
 710                    arenderer.SetBackground(vedo.get_color(self.backgrcol))
 711                    if bg2:
 712                        arenderer.GradientBackgroundOn()
 713                        arenderer.SetBackground2(vedo.get_color(bg2))
 714
 715                    x0 = i / shape[0]
 716                    y0 = j / shape[1]
 717                    x1 = (i + 1) / shape[0]
 718                    y1 = (j + 1) / shape[1]
 719                    arenderer.SetViewport(y0, x0, y1, x1)
 720                    self.renderers.append(arenderer)
 721                    self.axes_instances.append(None)
 722            self.shape = shape
 723
 724        if self.renderers:
 725            self.renderer = self.renderers[0]
 726            self.camera.SetParallelProjection(vedo.settings.use_parallel_projection)
 727
 728        #########################################################
 729        if self.qt_widget or self.wx_widget:
 730            if self.qt_widget:
 731                self.window = self.qt_widget.GetRenderWindow()  # overwrite
 732            else:
 733                self.window = self.wx_widget.GetRenderWindow()
 734            self.interactor = self.window.GetInteractor()
 735
 736        #########################################################
 737        for r in self.renderers:
 738            self.window.AddRenderer(r)
 739            # set the background gradient if any
 740            if vedo.settings.background_gradient_orientation > 0:
 741                try:
 742                    modes = [
 743                        vtki.vtkViewport.GradientModes.VTK_GRADIENT_VERTICAL,
 744                        vtki.vtkViewport.GradientModes.VTK_GRADIENT_HORIZONTAL,
 745                        vtki.vtkViewport.GradientModes.VTK_GRADIENT_RADIAL_VIEWPORT_FARTHEST_SIDE,
 746                        vtki.vtkViewport.GradientModes.VTK_GRADIENT_RADIAL_VIEWPORT_FARTHEST_CORNER,
 747                    ]
 748                    r.SetGradientMode(modes[vedo.settings.background_gradient_orientation])
 749                    r.GradientBackgroundOn()
 750                except AttributeError:
 751                    pass
 752
 753        #########################################################
 754        if self.qt_widget or self.wx_widget:
 755            # self.window.SetSize(int(self.size[0]), int(self.size[1]))
 756            self.interactor.SetRenderWindow(self.window)
 757            # vsty = vtki.new("InteractorStyleTrackballCamera")
 758            # self.interactor.SetInteractorStyle(vsty)
 759            if vedo.settings.enable_default_keyboard_callbacks:
 760                self.interactor.AddObserver("KeyPressEvent", self._default_keypress)
 761            if vedo.settings.enable_default_mouse_callbacks:
 762                self.interactor.AddObserver("LeftButtonPressEvent", self._default_mouseleftclick)
 763            return  ################
 764            ########################
 765
 766        if self.size[0] == "f":  # full screen
 767            self.size = "fullscreen"
 768            self.window.SetFullScreen(True)
 769            self.window.BordersOn()
 770        else:
 771            self.window.SetSize(int(self.size[0]), int(self.size[1]))
 772
 773        if self.offscreen:
 774            if self.axes in (4, 5, 8, 12, 14):
 775                self.axes = 0  # does not work with those
 776            self.window.SetOffScreenRendering(True)
 777            self.interactor = None
 778            self._interactive = False
 779            return  ################
 780            ########################
 781
 782        self.window.SetPosition(pos)
 783
 784        #########################################################
 785        self.interactor = vtki.vtkRenderWindowInteractor()
 786
 787        self.interactor.SetRenderWindow(self.window)
 788        vsty = vtki.new("InteractorStyleTrackballCamera")
 789        self.interactor.SetInteractorStyle(vsty)
 790        self.interactor.RemoveObservers("CharEvent")
 791
 792        if vedo.settings.enable_default_keyboard_callbacks:
 793            self.interactor.AddObserver("KeyPressEvent", self._default_keypress)
 794        if vedo.settings.enable_default_mouse_callbacks:
 795            self.interactor.AddObserver("LeftButtonPressEvent", self._default_mouseleftclick)
 796
 797    ##################################################################### ..init ends here.
 798
 799    def __str__(self):
 800        """Return Plotter info."""
 801        axtype = {
 802            0: "(no axes)",
 803            1: "(default customizable grid walls)",
 804            2: "(cartesian axes from origin",
 805            3: "(positive range of cartesian axes from origin",
 806            4: "(axes triad at bottom left)",
 807            5: "(oriented cube at bottom left)",
 808            6: "(mark the corners of the bounding box)",
 809            7: "(3D ruler at each side of the cartesian axes)",
 810            8: "(the vtkCubeAxesActor object)",
 811            9: "(the bounding box outline)",
 812            10: "(circles of maximum bounding box range)",
 813            11: "(show a large grid on the x-y plane)",
 814            12: "(show polar axes)",
 815            13: "(simple ruler at the bottom of the window)",
 816            14: "(the vtkCameraOrientationWidget object)",
 817        }
 818
 819        module = self.__class__.__module__
 820        name = self.__class__.__name__
 821        out = vedo.printc(
 822            f"{module}.{name} at ({hex(id(self))})".ljust(75),
 823            bold=True, invert=True, return_string=True,
 824        )
 825        out += "\x1b[0m"
 826        if self.interactor:
 827            out += "window title".ljust(14) + ": " + self.title + "\n"
 828            out += "window size".ljust(14) + f": {self.window.GetSize()}"
 829            out += f", full_screen={self.window.GetScreenSize()}\n"
 830            out += "activ renderer".ljust(14) + ": nr." + str(self.renderers.index(self.renderer))
 831            out += f" (out of {len(self.renderers)} renderers)\n"
 832
 833        bns, totpt = [], 0
 834        for a in self.objects:
 835            try:
 836                b = a.bounds()
 837                bns.append(b)
 838            except AttributeError:
 839                pass
 840            try:
 841                totpt += a.npoints
 842            except AttributeError:
 843                pass
 844        out += "n. of objects".ljust(14) + f": {len(self.objects)}"
 845        out += f" ({totpt} vertices)\n" if totpt else "\n"
 846
 847        if len(bns) > 0:
 848            min_bns = np.min(bns, axis=0)
 849            max_bns = np.max(bns, axis=0)
 850            bx1, bx2 = utils.precision(min_bns[0], 3), utils.precision(max_bns[1], 3)
 851            by1, by2 = utils.precision(min_bns[2], 3), utils.precision(max_bns[3], 3)
 852            bz1, bz2 = utils.precision(min_bns[4], 3), utils.precision(max_bns[5], 3)
 853            out += "bounds".ljust(14) + ":"
 854            out += " x=(" + bx1 + ", " + bx2 + "),"
 855            out += " y=(" + by1 + ", " + by2 + "),"
 856            out += " z=(" + bz1 + ", " + bz2 + ")\n"
 857
 858        if utils.is_integer(self.axes):
 859            out += "axes style".ljust(14) + f": {self.axes} {axtype[self.axes]}\n"
 860        elif isinstance(self.axes, dict):
 861            out += "axes style".ljust(14) + f": 1 {axtype[1]}\n"
 862        else:
 863            out += "axes style".ljust(14) + f": {[self.axes]}\n"
 864        return out.rstrip() + "\x1b[0m"
 865
 866    def print(self):
 867        """Print information about the current instance."""
 868        print(self.__str__())
 869        return self
 870
 871    def __iadd__(self, objects):
 872        self.add(objects)
 873        return self
 874
 875    def __isub__(self, objects):
 876        self.remove(objects)
 877        return self
 878
 879    def __enter__(self):
 880        # context manager like in "with Plotter() as plt:"
 881        return self
 882
 883    def __exit__(self, *args, **kwargs):
 884        # context manager like in "with Plotter() as plt:"
 885        self.close()
 886
 887    def initialize_interactor(self) -> Self:
 888        """Initialize the interactor if not already initialized."""
 889        if self.offscreen:
 890            return self
 891        if self.interactor:
 892            if not self.interactor.GetInitialized():
 893                self.interactor.Initialize()
 894                self.interactor.RemoveObservers("CharEvent")
 895        return self
 896
 897    def process_events(self) -> Self:
 898        """Process all pending events."""
 899        self.initialize_interactor()
 900        if self.interactor:
 901            try:
 902                self.interactor.ProcessEvents()
 903            except AttributeError:
 904                pass
 905        return self
 906
 907    def at(self, nren: int, yren=None) -> Self:
 908        """
 909        Select the current renderer number as an int.
 910        Can also use the `[nx, ny]` format.
 911        """
 912        if utils.is_sequence(nren):
 913            if len(nren) == 2:
 914                nren, yren = nren
 915            else:
 916                vedo.logger.error("at() argument must be a single number or a list of two numbers")
 917                raise RuntimeError
 918
 919        if yren is not None:
 920            a, b = self.shape
 921            x, y = nren, yren
 922            nren = x * b + y
 923            # print("at (", x, y, ")  -> ren", nren)
 924            if nren < 0 or nren > len(self.renderers) or x >= a or y >= b:
 925                vedo.logger.error(f"at({nren, yren}) is malformed!")
 926                raise RuntimeError
 927
 928        self.renderer = self.renderers[nren]
 929        return self
 930
 931    def add(self, *objs, at=None) -> Self:
 932        """
 933        Append the input objects to the internal list of objects to be shown.
 934
 935        Arguments:
 936            at : (int)
 937                add the object at the specified renderer
 938        """
 939        if at is not None:
 940            ren = self.renderers[at]
 941        else:
 942            ren = self.renderer
 943
 944        objs = utils.flatten(objs)
 945        for ob in objs:
 946            if ob and ob not in self.objects:
 947                self.objects.append(ob)
 948
 949        acts = self._scan_input_return_acts(objs)
 950
 951        for a in acts:
 952
 953            if ren:
 954                if isinstance(a, vedo.addons.BaseCutter):
 955                    a.add_to(self)  # from cutters
 956                    continue
 957
 958                if isinstance(a, vtki.vtkLight):
 959                    ren.AddLight(a)
 960                    continue
 961
 962                try:
 963                    ren.AddActor(a)
 964                except TypeError:
 965                    ren.AddActor(a.actor)
 966
 967                try:
 968                    ir = self.renderers.index(ren)
 969                    a.rendered_at.add(ir) # might not have rendered_at
 970                except (AttributeError, ValueError):
 971                    pass
 972
 973                if isinstance(a, vtki.vtkFollower):
 974                    a.SetCamera(self.camera)
 975                elif isinstance(a, vedo.visual.LightKit):
 976                    a.lightkit.AddLightsToRenderer(ren)
 977
 978        return self
 979
 980    def remove(self, *objs, at=None) -> Self:
 981        """
 982        Remove input object to the internal list of objects to be shown.
 983
 984        Objects to be removed can be referenced by their assigned name,
 985
 986        Arguments:
 987            at : (int)
 988                remove the object at the specified renderer
 989        """
 990        # TODO and you can also use wildcards like `*` and `?`.
 991        if at is not None:
 992            ren = self.renderers[at]
 993        else:
 994            ren = self.renderer
 995
 996        objs = [ob for ob in utils.flatten(objs) if ob]
 997
 998        has_str = False
 999        for ob in objs:
1000            if isinstance(ob, str):
1001                has_str = True
1002                break
1003
1004        has_actor = False
1005        for ob in objs:
1006            if hasattr(ob, "actor") and ob.actor:
1007                has_actor = True
1008                break
1009
1010        if has_str or has_actor:
1011            # need to get the actors to search for
1012            for a in self.get_actors(include_non_pickables=True):
1013                # print("PARSING", [a])
1014                try:
1015                    if (a.name and a.name in objs) or a in objs:
1016                        objs.append(a)
1017                    # if a.name:
1018                    #     bools = [utils.parse_pattern(ob, a.name)[0] for ob in objs]
1019                    #     if any(bools) or a in objs:
1020                    #         objs.append(a)
1021                    #     print('a.name',a.name, objs,any(bools))
1022                except AttributeError:  # no .name
1023                    # passing the actor so get back the object with .retrieve_object()
1024                    try:
1025                        vobj = a.retrieve_object()
1026                        if (vobj.name and vobj.name in objs) or vobj in objs:
1027                            # print('vobj.name', vobj.name)
1028                            objs.append(vobj)
1029                    except AttributeError:
1030                        pass
1031
1032        ir = self.renderers.index(ren)
1033
1034        ids = []
1035        for ob in set(objs):
1036
1037            # will remove it from internal list if possible
1038            try:
1039                idx = self.objects.index(ob)
1040                ids.append(idx)
1041            except ValueError:
1042                pass
1043
1044            if ren:  ### remove it from the renderer
1045
1046                if isinstance(ob, vedo.addons.BaseCutter):
1047                    ob.remove_from(self)  # from cutters
1048                    continue
1049
1050                try:
1051                    ren.RemoveActor(ob)
1052                except TypeError:
1053                    try:
1054                        ren.RemoveActor(ob.actor)
1055                    except AttributeError:
1056                        pass
1057
1058                if hasattr(ob, "rendered_at"):
1059                    ob.rendered_at.discard(ir)
1060
1061                if hasattr(ob, "scalarbar") and ob.scalarbar:
1062                    ren.RemoveActor(ob.scalarbar)
1063                if hasattr(ob, "_caption") and ob._caption:
1064                    ren.RemoveActor(ob._caption)
1065                if hasattr(ob, "shadows") and ob.shadows:
1066                    for sha in ob.shadows:
1067                        ren.RemoveActor(sha.actor)
1068                if hasattr(ob, "trail") and ob.trail:
1069                    ren.RemoveActor(ob.trail.actor)
1070                    ob.trail_points = []
1071                    if hasattr(ob.trail, "shadows") and ob.trail.shadows:
1072                        for sha in ob.trail.shadows:
1073                            ren.RemoveActor(sha.actor)
1074
1075                elif isinstance(ob, vedo.visual.LightKit):
1076                    ob.lightkit.RemoveLightsFromRenderer(ren)
1077
1078        # for i in ids: # WRONG way of doing it!
1079        #     del self.objects[i]
1080        # instead we do:
1081        self.objects = [ele for i, ele in enumerate(self.objects) if i not in ids]
1082        return self
1083
1084    @property
1085    def actors(self):
1086        """Return the list of actors."""
1087        return [ob.actor for ob in self.objects if hasattr(ob, "actor")]
1088
1089    def remove_lights(self) -> Self:
1090        """Remove all the present lights in the current renderer."""
1091        if self.renderer:
1092            self.renderer.RemoveAllLights()
1093        return self
1094
1095    def pop(self, at=None) -> Self:
1096        """
1097        Remove the last added object from the rendering window.
1098        This method is typically used in loops or callback functions.
1099        """
1100        if at is not None and not isinstance(at, int):
1101            # wrong usage pitfall
1102            vedo.logger.error("argument of pop() must be an integer")
1103            raise RuntimeError()
1104
1105        if self.objects:
1106            self.remove(self.objects[-1], at)
1107        return self
1108
1109    def render(self, resetcam=False) -> Self:
1110        """Render the scene. This method is typically used in loops or callback functions."""
1111
1112        if vedo.settings.dry_run_mode >= 2:
1113            return self
1114
1115        if not self.window:
1116            return self
1117
1118        self.initialize_interactor()
1119
1120        if resetcam:
1121            self.renderer.ResetCamera()
1122
1123        self.window.Render()
1124
1125        if self._cocoa_process_events and self.interactor and self.interactor.GetInitialized():
1126            if "Darwin" in vedo.sys_platform and not self.offscreen:
1127                self.interactor.ProcessEvents()
1128                self._cocoa_process_events = False
1129        return self
1130
1131    def interactive(self) -> Self:
1132        """
1133        Start window interaction.
1134        Analogous to `show(..., interactive=True)`.
1135        """
1136        if vedo.settings.dry_run_mode >= 1:
1137            return self
1138        self.initialize_interactor()
1139        if self.interactor:
1140            # print("self.interactor.Start()")
1141            self.interactor.Start()
1142            # print("self.interactor.Start() done")
1143            if self._must_close_now:
1144                # print("self.interactor.TerminateApp()")
1145                if self.interactor:
1146                    self.interactor.GetRenderWindow().Finalize()
1147                    self.interactor.TerminateApp()
1148                self.interactor = None
1149                self.window = None
1150                self.renderer = None
1151                self.renderers = []
1152                self.camera = None
1153        return self
1154
1155    def use_depth_peeling(self, at=None, value=True) -> Self:
1156        """
1157        Specify whether use depth peeling algorithm at this specific renderer
1158        Call this method before the first rendering.
1159        """
1160        if at is None:
1161            ren = self.renderer
1162        else:
1163            ren = self.renderers[at]
1164        ren.SetUseDepthPeeling(value)
1165        return self
1166
1167    def background(self, c1=None, c2=None, at=None, mode=0) -> Union[Self, "np.ndarray"]:
1168        """Set the color of the background for the current renderer.
1169        A different renderer index can be specified by keyword `at`.
1170
1171        Arguments:
1172            c1 : (list)
1173                background main color.
1174            c2 : (list)
1175                background color for the upper part of the window.
1176            at : (int)
1177                renderer index.
1178            mode : (int)
1179                background mode (needs vtk version >= 9.3)
1180                    0 = vertical,
1181                    1 = horizontal,
1182                    2 = radial farthest side,
1183                    3 = radia farthest corner.
1184        """
1185        if not self.renderers:
1186            return self
1187        if at is None:
1188            r = self.renderer
1189        else:
1190            r = self.renderers[at]
1191
1192        if c1 is None and c2 is None:
1193            return np.array(r.GetBackground())
1194
1195        if r:
1196            if c1 is not None:
1197                r.SetBackground(vedo.get_color(c1))
1198            if c2 is not None:
1199                r.GradientBackgroundOn()
1200                r.SetBackground2(vedo.get_color(c2))
1201                if mode:
1202                    try:  # only works with vtk>=9.3
1203                        modes = [
1204                            vtki.vtkViewport.GradientModes.VTK_GRADIENT_VERTICAL,
1205                            vtki.vtkViewport.GradientModes.VTK_GRADIENT_HORIZONTAL,
1206                            vtki.vtkViewport.GradientModes.VTK_GRADIENT_RADIAL_VIEWPORT_FARTHEST_SIDE,
1207                            vtki.vtkViewport.GradientModes.VTK_GRADIENT_RADIAL_VIEWPORT_FARTHEST_CORNER,
1208                        ]
1209                        r.SetGradientMode(modes[vedo.settings.background_gradient_orientation])
1210                    except AttributeError:
1211                        pass
1212
1213            else:
1214                r.GradientBackgroundOff()
1215        return self
1216
1217    ##################################################################
1218    def get_meshes(self, at=None, include_non_pickables=False, unpack_assemblies=True) -> list:
1219        """
1220        Return a list of Meshes from the specified renderer.
1221
1222        Arguments:
1223            at : (int)
1224                specify which renderer to look at.
1225            include_non_pickables : (bool)
1226                include non-pickable objects
1227            unpack_assemblies : (bool)
1228                unpack assemblies into their components
1229        """
1230        if at is None:
1231            renderer = self.renderer
1232            at = self.renderers.index(renderer)
1233        elif isinstance(at, int):
1234            renderer = self.renderers[at]
1235
1236        has_global_axes = False
1237        if isinstance(self.axes_instances[at], vedo.Assembly):
1238            has_global_axes = True
1239
1240        if unpack_assemblies:
1241            acs = renderer.GetActors()
1242        else:
1243            acs = renderer.GetViewProps()
1244
1245        objs = []
1246        acs.InitTraversal()
1247        for _ in range(acs.GetNumberOfItems()):
1248
1249            if unpack_assemblies:
1250                a = acs.GetNextItem()
1251            else:
1252                a = acs.GetNextProp()
1253
1254            if isinstance(a, vtki.vtkVolume):
1255                continue
1256
1257            if include_non_pickables or a.GetPickable():
1258                if a == self.axes_instances[at]:
1259                    continue
1260                if has_global_axes and a in self.axes_instances[at].actors:
1261                    continue
1262                try:
1263                    objs.append(a.retrieve_object())
1264                except AttributeError:
1265                    pass
1266        return objs
1267
1268    def get_volumes(self, at=None, include_non_pickables=False) -> list:
1269        """
1270        Return a list of Volumes from the specified renderer.
1271
1272        Arguments:
1273            at : (int)
1274                specify which renderer to look at
1275            include_non_pickables : (bool)
1276                include non-pickable objects
1277        """
1278        if at is None:
1279            renderer = self.renderer
1280            at = self.renderers.index(renderer)
1281        elif isinstance(at, int):
1282            renderer = self.renderers[at]
1283
1284        vols = []
1285        acs = renderer.GetVolumes()
1286        acs.InitTraversal()
1287        for _ in range(acs.GetNumberOfItems()):
1288            a = acs.GetNextItem()
1289            if include_non_pickables or a.GetPickable():
1290                try:
1291                    vols.append(a.retrieve_object())
1292                except AttributeError:
1293                    pass
1294        return vols
1295
1296    def get_actors(self, at=None, include_non_pickables=False) -> list:
1297        """
1298        Return a list of Volumes from the specified renderer.
1299
1300        Arguments:
1301            at : (int)
1302                specify which renderer to look at
1303            include_non_pickables : (bool)
1304                include non-pickable objects
1305        """
1306        if at is None:
1307            renderer = self.renderer
1308            at = self.renderers.index(renderer)
1309        elif isinstance(at, int):
1310            renderer = self.renderers[at]
1311
1312        acts = []
1313        acs = renderer.GetViewProps()
1314        acs.InitTraversal()
1315        for _ in range(acs.GetNumberOfItems()):
1316            a = acs.GetNextProp()
1317            if include_non_pickables or a.GetPickable():
1318                acts.append(a)
1319        return acts
1320    
1321    def check_actors_trasform(self, at=None) -> Self:
1322        """
1323        Reset the transformation matrix of all actors at specified renderer.
1324        This is only useful when actors have been moved/rotated/scaled manually
1325        in an already rendered scene using interactors like
1326        'TrackballActor' or 'JoystickActor'.
1327        """
1328        # see issue https://github.com/marcomusy/vedo/issues/1046
1329        for a in self.get_actors(at=at, include_non_pickables=True):
1330            try:
1331                M = a.GetMatrix()
1332            except AttributeError:
1333                continue
1334            if M and not M.IsIdentity():
1335                try:
1336                    a.retrieve_object().apply_transform_from_actor()
1337                    # vedo.logger.info(
1338                    #     f"object '{a.retrieve_object().name}' "
1339                    #     "was manually moved. Updated to its current position."
1340                    # )
1341                except AttributeError:
1342                    pass
1343        return self
1344
1345    def reset_camera(self, tight=None) -> Self:
1346        """
1347        Reset the camera position and zooming.
1348        If tight (float) is specified the zooming reserves a padding space
1349        in the xy-plane expressed in percent of the average size.
1350        """
1351        if tight is None:
1352            self.renderer.ResetCamera()
1353        else:
1354            x0, x1, y0, y1, z0, z1 = self.renderer.ComputeVisiblePropBounds()
1355            cam = self.camera
1356
1357            self.renderer.ComputeAspect()
1358            aspect = self.renderer.GetAspect()
1359            angle = np.pi * cam.GetViewAngle() / 180.0
1360            dx = x1 - x0
1361            dy = y1 - y0
1362            dist = max(dx / aspect[0], dy) / np.tan(angle / 2) / 2
1363
1364            cam.SetViewUp(0, 1, 0)
1365            cam.SetPosition(x0 + dx / 2, y0 + dy / 2, dist * (1 + tight))
1366            cam.SetFocalPoint(x0 + dx / 2, y0 + dy / 2, 0)
1367            if cam.GetParallelProjection():
1368                ps = max(dx / aspect[0], dy) / 2
1369                cam.SetParallelScale(ps * (1 + tight))
1370            self.renderer.ResetCameraClippingRange(x0, x1, y0, y1, z0, z1)
1371        return self
1372    
1373    def reset_clipping_range(self, bounds=None) -> Self:
1374        """
1375        Reset the camera clipping range to include all visible actors.
1376        If bounds is given, it will be used instead of computing it.
1377        """
1378        if bounds is None:
1379            self.renderer.ResetCameraClippingRange()
1380        else:
1381            self.renderer.ResetCameraClippingRange(bounds)
1382        return self
1383
1384    def reset_viewup(self, smooth=True) -> Self:
1385        """
1386        Reset the orientation of the camera to the closest orthogonal direction and view-up.
1387        """
1388        vbb = addons.compute_visible_bounds()[0]
1389        x0, x1, y0, y1, z0, z1 = vbb
1390        mx, my, mz = (x0 + x1) / 2, (y0 + y1) / 2, (z0 + z1) / 2
1391        d = self.camera.GetDistance()
1392
1393        viewups = np.array(
1394            [(0, 1, 0), (0, -1, 0), (0, 0, 1), (0, 0, -1), (1, 0, 0), (-1, 0, 0)]
1395        )
1396        positions = np.array(
1397            [
1398                (mx, my, mz + d),
1399                (mx, my, mz - d),
1400                (mx, my + d, mz),
1401                (mx, my - d, mz),
1402                (mx + d, my, mz),
1403                (mx - d, my, mz),
1404            ]
1405        )
1406
1407        vu = np.array(self.camera.GetViewUp())
1408        vui = np.argmin(np.linalg.norm(viewups - vu, axis=1))
1409
1410        poc = np.array(self.camera.GetPosition())
1411        foc = np.array(self.camera.GetFocalPoint())
1412        a = poc - foc
1413        b = positions - foc
1414        a = a / np.linalg.norm(a)
1415        b = b.T * (1 / np.linalg.norm(b, axis=1))
1416        pui = np.argmin(np.linalg.norm(b.T - a, axis=1))
1417
1418        if smooth:
1419            outtimes = np.linspace(0, 1, num=11, endpoint=True)
1420            for t in outtimes:
1421                vv = vu * (1 - t) + viewups[vui] * t
1422                pp = poc * (1 - t) + positions[pui] * t
1423                ff = foc * (1 - t) + np.array([mx, my, mz]) * t
1424                self.camera.SetViewUp(vv)
1425                self.camera.SetPosition(pp)
1426                self.camera.SetFocalPoint(ff)
1427                self.render()
1428
1429            # interpolator does not respect parallel view...:
1430            # cam1 = dict(
1431            #     pos=poc,
1432            #     viewup=vu,
1433            #     focal_point=(mx,my,mz),
1434            #     clipping_range=self.camera.GetClippingRange()
1435            # )
1436            # # cam1 = self.camera
1437            # cam2 = dict(
1438            #     pos=positions[pui],
1439            #     viewup=viewups[vui],
1440            #     focal_point=(mx,my,mz),
1441            #     clipping_range=self.camera.GetClippingRange()
1442            # )
1443            # vcams = self.move_camera([cam1, cam2], output_times=outtimes, smooth=0)
1444            # for c in vcams:
1445            #     self.renderer.SetActiveCamera(c)
1446            #     self.render()
1447        else:
1448
1449            self.camera.SetViewUp(viewups[vui])
1450            self.camera.SetPosition(positions[pui])
1451            self.camera.SetFocalPoint(mx, my, mz)
1452
1453        self.renderer.ResetCameraClippingRange()
1454
1455        # vbb, _, _, _ = addons.compute_visible_bounds()
1456        # x0,x1, y0,y1, z0,z1 = vbb
1457        # self.renderer.ResetCameraClippingRange(x0, x1, y0, y1, z0, z1)
1458        self.render()
1459        return self
1460
1461    def move_camera(self, cameras, t=0, times=(), smooth=True, output_times=()) -> list:
1462        """
1463        Takes as input two cameras set camera at an interpolated position:
1464
1465        Cameras can be vtkCamera or dictionaries in format:
1466
1467            `dict(pos=..., focal_point=..., viewup=..., distance=..., clipping_range=...)`
1468
1469        Press `shift-C` key in interactive mode to dump a python snipplet
1470        of parameters for the current camera view.
1471        """
1472        nc = len(cameras)
1473        if len(times) == 0:
1474            times = np.linspace(0, 1, num=nc, endpoint=True)
1475
1476        assert len(times) == nc
1477
1478        cin = vtki.new("CameraInterpolator")
1479
1480        # cin.SetInterpolationTypeToLinear() # buggy?
1481        if nc > 2 and smooth:
1482            cin.SetInterpolationTypeToSpline()
1483
1484        for i, cam in enumerate(cameras):
1485            vcam = cam
1486            if isinstance(cam, dict):
1487                vcam = utils.camera_from_dict(cam)
1488            cin.AddCamera(times[i], vcam)
1489
1490        mint, maxt = cin.GetMinimumT(), cin.GetMaximumT()
1491        rng = maxt - mint
1492
1493        if len(output_times) == 0:
1494            cin.InterpolateCamera(t * rng, self.camera)
1495            return [self.camera]
1496        else:
1497            vcams = []
1498            for tt in output_times:
1499                c = vtki.vtkCamera()
1500                cin.InterpolateCamera(tt * rng, c)
1501                vcams.append(c)
1502            return vcams
1503
1504    def fly_to(self, point) -> Self:
1505        """
1506        Fly camera to the specified point.
1507
1508        Arguments:
1509            point : (list)
1510                point in space to place camera.
1511
1512        Example:
1513            ```python
1514            from vedo import *
1515            cone = Cone()
1516            plt = Plotter(axes=1)
1517            plt.show(cone)
1518            plt.fly_to([1,0,0])
1519            plt.interactive().close()
1520            ```
1521        """
1522        if self.interactor:
1523            self.resetcam = False
1524            self.interactor.FlyTo(self.renderer, point)
1525        return self
1526
1527    def look_at(self, plane="xy") -> Self:
1528        """Move the camera so that it looks at the specified cartesian plane"""
1529        cam = self.renderer.GetActiveCamera()
1530        fp = np.array(cam.GetFocalPoint())
1531        p = np.array(cam.GetPosition())
1532        dist = np.linalg.norm(fp - p)
1533        plane = plane.lower()
1534        if "x" in plane and "y" in plane:
1535            cam.SetPosition(fp[0], fp[1], fp[2] + dist)
1536            cam.SetViewUp(0.0, 1.0, 0.0)
1537        elif "x" in plane and "z" in plane:
1538            cam.SetPosition(fp[0], fp[1] - dist, fp[2])
1539            cam.SetViewUp(0.0, 0.0, 1.0)
1540        elif "y" in plane and "z" in plane:
1541            cam.SetPosition(fp[0] + dist, fp[1], fp[2])
1542            cam.SetViewUp(0.0, 0.0, 1.0)
1543        else:
1544            vedo.logger.error(f"in plotter.look() cannot understand argument {plane}")
1545        return self
1546
1547    def record(self, filename="") -> str:
1548        """
1549        Record camera, mouse, keystrokes and all other events.
1550        Recording can be toggled on/off by pressing key "R".
1551
1552        Arguments:
1553            filename : (str)
1554                ascii file to store events.
1555                The default is `vedo.settings.cache_directory+"vedo/recorded_events.log"`.
1556
1557        Returns:
1558            a string descriptor of events.
1559
1560        Examples:
1561            - [record_play.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/record_play.py)
1562        """
1563        if vedo.settings.dry_run_mode >= 1:
1564            return ""
1565        if not self.interactor:
1566            vedo.logger.warning("Cannot record events, no interactor defined.")
1567            return ""
1568        erec = vtki.new("InteractorEventRecorder")
1569        erec.SetInteractor(self.interactor)
1570        if not filename:
1571            if not os.path.exists(vedo.settings.cache_directory):
1572                os.makedirs(vedo.settings.cache_directory)
1573            home_dir = os.path.expanduser("~")
1574            filename = os.path.join(
1575                home_dir, vedo.settings.cache_directory, "vedo", "recorded_events.log")
1576            print("Events will be recorded in", filename)
1577        erec.SetFileName(filename)
1578        erec.SetKeyPressActivationValue("R")
1579        erec.EnabledOn()
1580        erec.Record()
1581        self.interactor.Start()
1582        erec.Stop()
1583        erec.EnabledOff()
1584        with open(filename, "r", encoding="UTF-8") as fl:
1585            events = fl.read()
1586        erec = None
1587        return events
1588
1589    def play(self, recorded_events="", repeats=0) -> Self:
1590        """
1591        Play camera, mouse, keystrokes and all other events.
1592
1593        Arguments:
1594            events : (str)
1595                file o string of events.
1596                The default is `vedo.settings.cache_directory+"vedo/recorded_events.log"`.
1597            repeats : (int)
1598                number of extra repeats of the same events. The default is 0.
1599
1600        Examples:
1601            - [record_play.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/record_play.py)
1602        """
1603        if vedo.settings.dry_run_mode >= 1:
1604            return self
1605        if not self.interactor:
1606            vedo.logger.warning("Cannot play events, no interactor defined.")
1607            return self
1608
1609        erec = vtki.new("InteractorEventRecorder")
1610        erec.SetInteractor(self.interactor)
1611
1612        if not recorded_events:
1613            home_dir = os.path.expanduser("~")
1614            recorded_events = os.path.join(
1615                home_dir, vedo.settings.cache_directory, "vedo", "recorded_events.log")
1616
1617        if recorded_events.endswith(".log"):
1618            erec.ReadFromInputStringOff()
1619            erec.SetFileName(recorded_events)
1620        else:
1621            erec.ReadFromInputStringOn()
1622            erec.SetInputString(recorded_events)
1623
1624        erec.Play()
1625        for _ in range(repeats):
1626            erec.Rewind()
1627            erec.Play()
1628        erec.EnabledOff()
1629        erec = None
1630        return self
1631
1632    def parallel_projection(self, value=True, at=None) -> Self:
1633        """
1634        Use parallel projection `at` a specified renderer.
1635        Object is seen from "infinite" distance, e.i. remove any perspective effects.
1636        An input value equal to -1 will toggle it on/off.
1637        """
1638        if at is not None:
1639            r = self.renderers[at]
1640        else:
1641            r = self.renderer
1642        if value == -1:
1643            val = r.GetActiveCamera().GetParallelProjection()
1644            value = not val
1645        r.GetActiveCamera().SetParallelProjection(value)
1646        r.Modified()
1647        return self
1648
1649    def render_hidden_lines(self, value=True) -> Self:
1650        """Remove hidden lines when in wireframe mode."""
1651        self.renderer.SetUseHiddenLineRemoval(not value)
1652        return self
1653
1654    def fov(self, angle: float) -> Self:
1655        """
1656        Set the field of view angle for the camera.
1657        This is the angle of the camera frustum in the horizontal direction.
1658        High values will result in a wide-angle lens (fish-eye effect),
1659        and low values will result in a telephoto lens.
1660
1661        Default value is 30 degrees.
1662        """
1663        self.renderer.GetActiveCamera().UseHorizontalViewAngleOn()
1664        self.renderer.GetActiveCamera().SetViewAngle(angle)
1665        return self
1666
1667    def zoom(self, zoom: float) -> Self:
1668        """Apply a zooming factor for the current camera view"""
1669        self.renderer.GetActiveCamera().Zoom(zoom)
1670        return self
1671
1672    def azimuth(self, angle: float) -> Self:
1673        """Rotate camera around the view up vector."""
1674        self.renderer.GetActiveCamera().Azimuth(angle)
1675        return self
1676
1677    def elevation(self, angle: float) -> Self:
1678        """Rotate the camera around the cross product of the negative
1679        of the direction of projection and the view up vector."""
1680        self.renderer.GetActiveCamera().Elevation(angle)
1681        return self
1682
1683    def roll(self, angle: float) -> Self:
1684        """Roll the camera about the direction of projection."""
1685        self.renderer.GetActiveCamera().Roll(angle)
1686        return self
1687
1688    def dolly(self, value: float) -> Self:
1689        """Move the camera towards (value>0) or away from (value<0) the focal point."""
1690        self.renderer.GetActiveCamera().Dolly(value)
1691        return self
1692
1693    ##################################################################
1694    def add_slider(
1695        self,
1696        sliderfunc,
1697        xmin,
1698        xmax,
1699        value=None,
1700        pos=4,
1701        title="",
1702        font="Calco",
1703        title_size=1,
1704        c=None,
1705        alpha=1,
1706        show_value=True,
1707        delayed=False,
1708        **options,
1709    ) -> "vedo.addons.Slider2D":
1710        """
1711        Add a `vedo.addons.Slider2D` which can call an external custom function.
1712
1713        Arguments:
1714            sliderfunc : (Callable)
1715                external function to be called by the widget
1716            xmin : (float)
1717                lower value of the slider
1718            xmax : (float)
1719                upper value
1720            value : (float)
1721                current value
1722            pos : (list, str)
1723                position corner number: horizontal [1-5] or vertical [11-15]
1724                it can also be specified by corners coordinates [(x1,y1), (x2,y2)]
1725                and also by a string descriptor (eg. "bottom-left")
1726            title : (str)
1727                title text
1728            font : (str)
1729                title font face. Check [available fonts here](https://vedo.embl.es/fonts).
1730            title_size : (float)
1731                title text scale [1.0]
1732            show_value : (bool)
1733                if True current value is shown
1734            delayed : (bool)
1735                if True the callback is delayed until when the mouse button is released
1736            alpha : (float)
1737                opacity of the scalar bar texts
1738            slider_length : (float)
1739                slider length
1740            slider_width : (float)
1741                slider width
1742            end_cap_length : (float)
1743                length of the end cap
1744            end_cap_width : (float)
1745                width of the end cap
1746            tube_width : (float)
1747                width of the tube
1748            title_height : (float)
1749                width of the title
1750            tformat : (str)
1751                format of the title
1752
1753        Examples:
1754            - [sliders1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/sliders1.py)
1755            - [sliders2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/sliders2.py)
1756
1757            ![](https://user-images.githubusercontent.com/32848391/50738848-be033480-11d8-11e9-9b1a-c13105423a79.jpg)
1758        """
1759        if c is None:  # automatic black or white
1760            c = (0.8, 0.8, 0.8)
1761            if np.sum(vedo.get_color(self.backgrcol)) > 1.5:
1762                c = (0.2, 0.2, 0.2)
1763        else:
1764            c = vedo.get_color(c)
1765
1766        slider2d = addons.Slider2D(
1767            sliderfunc,
1768            xmin,
1769            xmax,
1770            value,
1771            pos,
1772            title,
1773            font,
1774            title_size,
1775            c,
1776            alpha,
1777            show_value,
1778            delayed,
1779            **options,
1780        )
1781
1782        if self.renderer:
1783            slider2d.renderer = self.renderer
1784            if self.interactor:
1785                slider2d.interactor = self.interactor
1786                slider2d.on()
1787                self.sliders.append([slider2d, sliderfunc])
1788        return slider2d
1789
1790    def add_slider3d(
1791        self,
1792        sliderfunc,
1793        pos1,
1794        pos2,
1795        xmin,
1796        xmax,
1797        value=None,
1798        s=0.03,
1799        t=1,
1800        title="",
1801        rotation=0.0,
1802        c=None,
1803        show_value=True,
1804    ) -> "vedo.addons.Slider3D":
1805        """
1806        Add a 3D slider widget which can call an external custom function.
1807
1808        Arguments:
1809            sliderfunc : (function)
1810                external function to be called by the widget
1811            pos1 : (list)
1812                first position 3D coordinates
1813            pos2 : (list)
1814                second position coordinates
1815            xmin : (float)
1816                lower value
1817            xmax : (float)
1818                upper value
1819            value : (float)
1820                initial value
1821            s : (float)
1822                label scaling factor
1823            t : (float)
1824                tube scaling factor
1825            title : (str)
1826                title text
1827            c : (color)
1828                slider color
1829            rotation : (float)
1830                title rotation around slider axis
1831            show_value : (bool)
1832                if True current value is shown
1833
1834        Examples:
1835            - [sliders3d.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/sliders3d.py)
1836
1837            ![](https://user-images.githubusercontent.com/32848391/52859555-4efcf200-312d-11e9-9290-6988c8295163.png)
1838        """
1839        if c is None:  # automatic black or white
1840            c = (0.8, 0.8, 0.8)
1841            if np.sum(vedo.get_color(self.backgrcol)) > 1.5:
1842                c = (0.2, 0.2, 0.2)
1843        else:
1844            c = vedo.get_color(c)
1845
1846        slider3d = addons.Slider3D(
1847            sliderfunc,
1848            pos1,
1849            pos2,
1850            xmin,
1851            xmax,
1852            value,
1853            s,
1854            t,
1855            title,
1856            rotation,
1857            c,
1858            show_value,
1859        )
1860        slider3d.renderer = self.renderer
1861        slider3d.interactor = self.interactor
1862        slider3d.on()
1863        self.sliders.append([slider3d, sliderfunc])
1864        return slider3d
1865
1866    def add_button(
1867        self,
1868        fnc=None,
1869        states=("On", "Off"),
1870        c=("w", "w"),
1871        bc=("green4", "red4"),
1872        pos=(0.7, 0.1),
1873        size=24,
1874        font="Courier",
1875        bold=True,
1876        italic=False,
1877        alpha=1,
1878        angle=0,
1879    ) -> Union["vedo.addons.Button", None]:
1880        """
1881        Add a button to the renderer window.
1882
1883        Arguments:
1884            states : (list)
1885                a list of possible states, e.g. ['On', 'Off']
1886            c : (list)
1887                a list of colors for each state
1888            bc : (list)
1889                a list of background colors for each state
1890            pos : (list)
1891                2D position from left-bottom corner
1892            size : (float)
1893                size of button font
1894            font : (str)
1895                font type. Check [available fonts here](https://vedo.embl.es/fonts).
1896            bold : (bool)
1897                bold font face (False)
1898            italic : (bool)
1899                italic font face (False)
1900            alpha : (float)
1901                opacity level
1902            angle : (float)
1903                anticlockwise rotation in degrees
1904
1905        Returns:
1906            `vedo.addons.Button` object.
1907
1908        Examples:
1909            - [buttons1.py](https://github.com/marcomusy/vedo/blob/master/examples/basic/buttons1.py)
1910            - [buttons2.py](https://github.com/marcomusy/vedo/blob/master/examples/basic/buttons2.py)
1911
1912            ![](https://user-images.githubusercontent.com/32848391/50738870-c0fe2500-11d8-11e9-9b78-92754f5c5968.jpg)
1913        """
1914        if self.interactor:
1915            bu = addons.Button(fnc, states, c, bc, pos, size, font, bold, italic, alpha, angle)
1916            self.renderer.AddActor2D(bu)
1917            self.buttons.append(bu)
1918            # bu.function_id = self.add_callback("LeftButtonPress", bu.function) # not good
1919            bu.function_id = bu.add_observer("pick", bu.function, priority=10)
1920            return bu
1921        return None
1922
1923    def add_spline_tool(
1924        self, points, pc="k", ps=8, lc="r4", ac="g5",
1925        lw=2, alpha=1, closed=False, ontop=True, can_add_nodes=True,
1926    ) -> "vedo.addons.SplineTool":
1927        """
1928        Add a spline tool to the current plotter.
1929        Nodes of the spline can be dragged in space with the mouse.
1930        Clicking on the line itself adds an extra point.
1931        Selecting a point and pressing del removes it.
1932
1933        Arguments:
1934            points : (Mesh, Points, array)
1935                the set of vertices forming the spline nodes.
1936            pc : (str)
1937                point color. The default is 'k'.
1938            ps : (str)
1939                point size. The default is 8.
1940            lc : (str)
1941                line color. The default is 'r4'.
1942            ac : (str)
1943                active point marker color. The default is 'g5'.
1944            lw : (int)
1945                line width. The default is 2.
1946            alpha : (float)
1947                line transparency.
1948            closed : (bool)
1949                spline is meant to be closed. The default is False.
1950
1951        Returns:
1952            a `SplineTool` object.
1953
1954        Examples:
1955            - [spline_tool.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/spline_tool.py)
1956
1957            ![](https://vedo.embl.es/images/basic/spline_tool.png)
1958        """
1959        sw = addons.SplineTool(points, pc, ps, lc, ac, lw, alpha, closed, ontop, can_add_nodes)
1960        sw.interactor = self.interactor
1961        sw.on()
1962        sw.Initialize(sw.points.dataset)
1963        sw.representation.SetRenderer(self.renderer)
1964        sw.representation.SetClosedLoop(closed)
1965        sw.representation.BuildRepresentation()
1966        self.widgets.append(sw)
1967        return sw
1968
1969    def add_icon(self, icon, pos=3, size=0.08) -> "vedo.addons.Icon":
1970        """Add an inset icon mesh into the same renderer.
1971
1972        Arguments:
1973            pos : (int, list)
1974                icon position in the range [1-4] indicating one of the 4 corners,
1975                or it can be a tuple (x,y) as a fraction of the renderer size.
1976            size : (float)
1977                size of the square inset.
1978
1979        Examples:
1980            - [icon.py](https://github.com/marcomusy/vedo/tree/master/examples/other/icon.py)
1981        """
1982        iconw = addons.Icon(icon, pos, size)
1983
1984        iconw.SetInteractor(self.interactor)
1985        iconw.EnabledOn()
1986        iconw.InteractiveOff()
1987        self.widgets.append(iconw)
1988        return iconw
1989
1990    def add_global_axes(self, axtype=None, c=None) -> Self:
1991        """Draw axes on scene. Available axes types:
1992
1993        Arguments:
1994            axtype : (int)
1995                - 0,  no axes,
1996                - 1,  draw three gray grid walls
1997                - 2,  show cartesian axes from (0,0,0)
1998                - 3,  show positive range of cartesian axes from (0,0,0)
1999                - 4,  show a triad at bottom left
2000                - 5,  show a cube at bottom left
2001                - 6,  mark the corners of the bounding box
2002                - 7,  draw a 3D ruler at each side of the cartesian axes
2003                - 8,  show the vtkCubeAxesActor object
2004                - 9,  show the bounding box outLine
2005                - 10, show three circles representing the maximum bounding box
2006                - 11, show a large grid on the x-y plane
2007                - 12, show polar axes
2008                - 13, draw a simple ruler at the bottom of the window
2009
2010            Axis type-1 can be fully customized by passing a dictionary axes=dict().
2011
2012        Example:
2013            ```python
2014            from vedo import Box, show
2015            b = Box(pos=(0, 0, 0), length=80, width=90, height=70).alpha(0.1)
2016            show(
2017                b,
2018                axes={
2019                    "xtitle": "Some long variable [a.u.]",
2020                    "number_of_divisions": 4,
2021                    # ...
2022                },
2023            )
2024            ```
2025
2026        Examples:
2027            - [custom_axes1.py](https://github.com/marcomusy/vedo/blob/master/examples/pyplot/custom_axes1.py)
2028            - [custom_axes2.py](https://github.com/marcomusy/vedo/blob/master/examples/pyplot/custom_axes2.py)
2029            - [custom_axes3.py](https://github.com/marcomusy/vedo/blob/master/examples/pyplot/custom_axes3.py)
2030            - [custom_axes4.py](https://github.com/marcomusy/vedo/blob/master/examples/pyplot/custom_axes4.py)
2031
2032            <img src="https://user-images.githubusercontent.com/32848391/72752870-ab7d5280-3bc3-11ea-8911-9ace00211e23.png" width="600">
2033        """
2034        addons.add_global_axes(axtype, c)
2035        return self
2036
2037    def add_legend_box(self, **kwargs) -> "vedo.addons.LegendBox":
2038        """Add a legend to the top right.
2039
2040        Examples:
2041            - [legendbox.py](https://github.com/marcomusy/vedo/blob/master/examples/examples/basic/legendbox.py),
2042            - [flag_labels1.py](https://github.com/marcomusy/vedo/blob/master/examples/examples/other/flag_labels1.py)
2043            - [flag_labels2.py](https://github.com/marcomusy/vedo/blob/master/examples/examples/other/flag_labels2.py)
2044        """
2045        acts = self.get_meshes()
2046        lb = addons.LegendBox(acts, **kwargs)
2047        self.add(lb)
2048        return lb
2049
2050    def add_hint(
2051        self,
2052        obj,
2053        text="",
2054        c="k",
2055        bg="yellow9",
2056        font="Calco",
2057        size=18,
2058        justify=0,
2059        angle=0,
2060        delay=250,
2061    ) -> Union[vtki.vtkBalloonWidget, None]:
2062        """
2063        Create a pop-up hint style message when hovering an object.
2064        Use `add_hint(obj, False)` to disable a hinting a specific object.
2065        Use `add_hint(None)` to disable all hints.
2066
2067        Arguments:
2068            obj : (Mesh, Points)
2069                the object to associate the pop-up to
2070            text : (str)
2071                string description of the pop-up
2072            delay : (int)
2073                milliseconds to wait before pop-up occurs
2074        """
2075        if self.offscreen or not self.interactor:
2076            return None
2077
2078        if vedo.vtk_version[:2] == (9, 0) and "Linux" in vedo.sys_platform:
2079            # Linux vtk9.0 is bugged
2080            vedo.logger.warning(
2081                f"add_hint() is not available on Linux platforms for vtk{vedo.vtk_version}."
2082            )
2083            return None
2084
2085        if obj is None:
2086            self.hint_widget.EnabledOff()
2087            self.hint_widget.SetInteractor(None)
2088            self.hint_widget = None
2089            return self.hint_widget
2090
2091        if text is False and self.hint_widget:
2092            self.hint_widget.RemoveBalloon(obj)
2093            return self.hint_widget
2094
2095        if text == "":
2096            if obj.name:
2097                text = obj.name
2098            elif obj.filename:
2099                text = obj.filename
2100            else:
2101                return None
2102
2103        if not self.hint_widget:
2104            self.hint_widget = vtki.vtkBalloonWidget()
2105
2106            rep = self.hint_widget.GetRepresentation()
2107            rep.SetBalloonLayoutToImageRight()
2108
2109            trep = rep.GetTextProperty()
2110            trep.SetFontFamily(vtki.VTK_FONT_FILE)
2111            trep.SetFontFile(utils.get_font_path(font))
2112            trep.SetFontSize(size)
2113            trep.SetColor(vedo.get_color(c))
2114            trep.SetBackgroundColor(vedo.get_color(bg))
2115            trep.SetShadow(0)
2116            trep.SetJustification(justify)
2117            trep.UseTightBoundingBoxOn()
2118
2119            self.hint_widget.ManagesCursorOff()
2120            self.hint_widget.SetTimerDuration(delay)
2121            self.hint_widget.SetInteractor(self.interactor)
2122            if angle:
2123                trep.SetOrientation(angle)
2124                trep.SetBackgroundOpacity(0)
2125            # else:
2126            #     trep.SetBackgroundOpacity(0.5) # doesnt work well
2127            self.hint_widget.SetRepresentation(rep)
2128            self.widgets.append(self.hint_widget)
2129            self.hint_widget.EnabledOn()
2130
2131        bst = self.hint_widget.GetBalloonString(obj.actor)
2132        if bst:
2133            self.hint_widget.UpdateBalloonString(obj.actor, text)
2134        else:
2135            self.hint_widget.AddBalloon(obj.actor, text)
2136
2137        return self.hint_widget
2138
2139    def add_shadows(self) -> Self:
2140        """Add shadows at the current renderer."""
2141        if self.renderer:
2142            shadows = vtki.new("ShadowMapPass")
2143            seq = vtki.new("SequencePass")
2144            passes = vtki.new("RenderPassCollection")
2145            passes.AddItem(shadows.GetShadowMapBakerPass())
2146            passes.AddItem(shadows)
2147            seq.SetPasses(passes)
2148            camerapass = vtki.new("CameraPass")
2149            camerapass.SetDelegatePass(seq)
2150            self.renderer.SetPass(camerapass)
2151        return self
2152
2153    def add_ambient_occlusion(self, radius: float, bias=0.01, blur=True, samples=100) -> Self:
2154        """
2155        Screen Space Ambient Occlusion.
2156
2157        For every pixel on the screen, the pixel shader samples the depth values around
2158        the current pixel and tries to compute the amount of occlusion from each of the sampled
2159        points.
2160
2161        Arguments:
2162            radius : (float)
2163                radius of influence in absolute units
2164            bias : (float)
2165                bias of the normals
2166            blur : (bool)
2167                add a blurring to the sampled positions
2168            samples : (int)
2169                number of samples to probe
2170
2171        Examples:
2172            - [ssao.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/ssao.py)
2173
2174            ![](https://vedo.embl.es/images/basic/ssao.jpg)
2175        """
2176        lights = vtki.new("LightsPass")
2177
2178        opaque = vtki.new("OpaquePass")
2179
2180        ssaoCam = vtki.new("CameraPass")
2181        ssaoCam.SetDelegatePass(opaque)
2182
2183        ssao = vtki.new("SSAOPass")
2184        ssao.SetRadius(radius)
2185        ssao.SetBias(bias)
2186        ssao.SetBlur(blur)
2187        ssao.SetKernelSize(samples)
2188        ssao.SetDelegatePass(ssaoCam)
2189
2190        translucent = vtki.new("TranslucentPass")
2191
2192        volpass = vtki.new("VolumetricPass")
2193        ddp = vtki.new("DualDepthPeelingPass")
2194        ddp.SetTranslucentPass(translucent)
2195        ddp.SetVolumetricPass(volpass)
2196
2197        over = vtki.new("OverlayPass")
2198
2199        collection = vtki.new("RenderPassCollection")
2200        collection.AddItem(lights)
2201        collection.AddItem(ssao)
2202        collection.AddItem(ddp)
2203        collection.AddItem(over)
2204
2205        sequence = vtki.new("SequencePass")
2206        sequence.SetPasses(collection)
2207
2208        cam = vtki.new("CameraPass")
2209        cam.SetDelegatePass(sequence)
2210
2211        self.renderer.SetPass(cam)
2212        return self
2213
2214    def add_depth_of_field(self, autofocus=True) -> Self:
2215        """Add a depth of field effect in the scene."""
2216        lights = vtki.new("LightsPass")
2217
2218        opaque = vtki.new("OpaquePass")
2219
2220        dofCam = vtki.new("CameraPass")
2221        dofCam.SetDelegatePass(opaque)
2222
2223        dof = vtki.new("DepthOfFieldPass")
2224        dof.SetAutomaticFocalDistance(autofocus)
2225        dof.SetDelegatePass(dofCam)
2226
2227        collection = vtki.new("RenderPassCollection")
2228        collection.AddItem(lights)
2229        collection.AddItem(dof)
2230
2231        sequence = vtki.new("SequencePass")
2232        sequence.SetPasses(collection)
2233
2234        cam = vtki.new("CameraPass")
2235        cam.SetDelegatePass(sequence)
2236
2237        self.renderer.SetPass(cam)
2238        return self
2239
2240    def _add_skybox(self, hdrfile: str) -> Self:
2241        # many hdr files are at https://polyhaven.com/all
2242
2243        reader = vtki.new("HDRReader")
2244        # Check the image can be read.
2245        if not reader.CanReadFile(hdrfile):
2246            vedo.logger.error(f"Cannot read HDR file {hdrfile}")
2247            return self
2248        reader.SetFileName(hdrfile)
2249        reader.Update()
2250
2251        texture = vtki.vtkTexture()
2252        texture.SetColorModeToDirectScalars()
2253        texture.SetInputData(reader.GetOutput())
2254
2255        # Convert to a cube map
2256        tcm = vtki.new("EquirectangularToCubeMapTexture")
2257        tcm.SetInputTexture(texture)
2258        # Enable mipmapping to handle HDR image
2259        tcm.MipmapOn()
2260        tcm.InterpolateOn()
2261
2262        self.renderer.SetEnvironmentTexture(tcm)
2263        self.renderer.UseImageBasedLightingOn()
2264        self.skybox = vtki.new("Skybox")
2265        self.skybox.SetTexture(tcm)
2266        self.renderer.AddActor(self.skybox)
2267        return self
2268
2269    def add_renderer_frame(self, c=None, alpha=None, lw=None, padding=None) -> "vedo.addons.RendererFrame":
2270        """
2271        Add a frame to the renderer subwindow.
2272
2273        Arguments:
2274            c : (color)
2275                color name or index
2276            alpha : (float)
2277                opacity level
2278            lw : (int)
2279                line width in pixels.
2280            padding : (float)
2281                padding space in pixels.
2282        """
2283        if c is None:  # automatic black or white
2284            c = (0.9, 0.9, 0.9)
2285            if self.renderer:
2286                if np.sum(self.renderer.GetBackground()) > 1.5:
2287                    c = (0.1, 0.1, 0.1)
2288        renf = addons.RendererFrame(c, alpha, lw, padding)
2289        if renf:
2290            self.renderer.AddActor(renf)
2291        return renf
2292
2293    def add_hover_legend(
2294        self,
2295        at=None,
2296        c=None,
2297        pos="bottom-left",
2298        font="Calco",
2299        s=0.75,
2300        bg="auto",
2301        alpha=0.1,
2302        maxlength=24,
2303        use_info=False,
2304    ) -> int:
2305        """
2306        Add a legend with 2D text which is triggered by hovering the mouse on an object.
2307
2308        The created text object are stored in `plotter.hover_legends`.
2309
2310        Returns:
2311            the id of the callback function.
2312
2313        Arguments:
2314            c : (color)
2315                Text color. If None then black or white is chosen automatically
2316            pos : (str)
2317                text positioning
2318            font : (str)
2319                text font type. Check [available fonts here](https://vedo.embl.es/fonts).
2320            s : (float)
2321                text size scale
2322            bg : (color)
2323                background color of the 2D box containing the text
2324            alpha : (float)
2325                box transparency
2326            maxlength : (int)
2327                maximum number of characters per line
2328            use_info : (bool)
2329                visualize the content of the `obj.info` attribute
2330
2331        Examples:
2332            - [hover_legend.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/hover_legend.py)
2333            - [earthquake_browser.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/earthquake_browser.py)
2334
2335            ![](https://vedo.embl.es/images/pyplot/earthquake_browser.jpg)
2336        """
2337        hoverlegend = vedo.shapes.Text2D(pos=pos, font=font, c=c, s=s, alpha=alpha, bg=bg)
2338
2339        if at is None:
2340            at = self.renderers.index(self.renderer)
2341
2342        def _legfunc(evt):
2343            if not evt.object or not self.renderer or at != evt.at:
2344                if hoverlegend.mapper.GetInput():  # clear and return
2345                    hoverlegend.mapper.SetInput("")
2346                    self.render()
2347                return
2348
2349            if use_info:
2350                if hasattr(evt.object, "info"):
2351                    t = str(evt.object.info)
2352                else:
2353                    return
2354            else:
2355                t, tp = "", ""
2356                if evt.isMesh:
2357                    tp = "Mesh "
2358                elif evt.isPoints:
2359                    tp = "Points "
2360                elif evt.isVolume:
2361                    tp = "Volume "
2362                elif evt.isImage:
2363                    tp = "Image "
2364                elif evt.isAssembly:
2365                    tp = "Assembly "
2366                else:
2367                    return
2368
2369                if evt.isAssembly:
2370                    if not evt.object.name:
2371                        t += f"Assembly object of {len(evt.object.unpack())} parts\n"
2372                    else:
2373                        t += f"Assembly name: {evt.object.name} ({len(evt.object.unpack())} parts)\n"
2374                else:
2375                    if evt.object.name:
2376                        t += f"{tp}name"
2377                        if evt.isPoints:
2378                            t += "  "
2379                        if evt.isMesh:
2380                            t += "  "
2381                        t += f": {evt.object.name[:maxlength]}".ljust(maxlength) + "\n"
2382
2383                if evt.object.filename:
2384                    t += f"{tp}filename: "
2385                    t += f"{os.path.basename(evt.object.filename[-maxlength:])}".ljust(maxlength)
2386                    t += "\n"
2387                    if not evt.object.file_size:
2388                        evt.object.file_size, evt.object.created = vedo.file_io.file_info(evt.object.filename)
2389                    if evt.object.file_size:
2390                        t += "             : "
2391                        sz, created = evt.object.file_size, evt.object.created
2392                        t += f"{created[4:-5]} ({sz})" + "\n"
2393
2394                if evt.isPoints:
2395                    indata = evt.object.dataset
2396                    if indata.GetNumberOfPoints():
2397                        t += (
2398                            f"#points/cells: {indata.GetNumberOfPoints()}"
2399                            f" / {indata.GetNumberOfCells()}"
2400                        )
2401                    pdata = indata.GetPointData()
2402                    cdata = indata.GetCellData()
2403                    if pdata.GetScalars() and pdata.GetScalars().GetName():
2404                        t += f"\nPoint array  : {pdata.GetScalars().GetName()}"
2405                        if pdata.GetScalars().GetName() == evt.object.mapper.GetArrayName():
2406                            t += " *"
2407                    if cdata.GetScalars() and cdata.GetScalars().GetName():
2408                        t += f"\nCell  array  : {cdata.GetScalars().GetName()}"
2409                        if cdata.GetScalars().GetName() == evt.object.mapper.GetArrayName():
2410                            t += " *"
2411
2412                if evt.isImage:
2413                    t = f"{os.path.basename(evt.object.filename[:maxlength+10])}".ljust(maxlength+10)
2414                    t += f"\nImage shape: {evt.object.shape}"
2415                    pcol = self.color_picker(evt.picked2d)
2416                    t += f"\nPixel color: {vedo.colors.rgb2hex(pcol/255)} {pcol}"
2417
2418            # change box color if needed in 'auto' mode
2419            if evt.isPoints and "auto" in str(bg):
2420                actcol = evt.object.properties.GetColor()
2421                if hoverlegend.mapper.GetTextProperty().GetBackgroundColor() != actcol:
2422                    hoverlegend.mapper.GetTextProperty().SetBackgroundColor(actcol)
2423
2424            # adapt to changes in bg color
2425            bgcol = self.renderers[at].GetBackground()
2426            _bgcol = c
2427            if _bgcol is None:  # automatic black or white
2428                _bgcol = (0.9, 0.9, 0.9)
2429                if sum(bgcol) > 1.5:
2430                    _bgcol = (0.1, 0.1, 0.1)
2431                if len(set(_bgcol).intersection(bgcol)) < 3:
2432                    hoverlegend.color(_bgcol)
2433
2434            if hoverlegend.mapper.GetInput() != t:
2435                hoverlegend.mapper.SetInput(t)
2436                self.interactor.Render()
2437            
2438            # print("ABORT", idcall, hoverlegend.actor.GetCommand(idcall))
2439            # hoverlegend.actor.GetCommand(idcall).AbortFlagOn()
2440
2441        self.add(hoverlegend, at=at)
2442        self.hover_legends.append(hoverlegend)
2443        idcall = self.add_callback("MouseMove", _legfunc)
2444        return idcall
2445
2446    def add_scale_indicator(
2447        self,
2448        pos=(0.7, 0.05),
2449        s=0.02,
2450        length=2,
2451        lw=4,
2452        c="k1",
2453        alpha=1,
2454        units="",
2455        gap=0.05,
2456    ) -> Union["vedo.visual.Actor2D", None]:
2457        """
2458        Add a Scale Indicator. Only works in parallel mode (no perspective).
2459
2460        Arguments:
2461            pos : (list)
2462                fractional (x,y) position on the screen.
2463            s : (float)
2464                size of the text.
2465            length : (float)
2466                length of the line.
2467            units : (str)
2468                string to show units.
2469            gap : (float)
2470                separation of line and text.
2471
2472        Example:
2473            ```python
2474            from vedo import settings, Cube, Plotter
2475            settings.use_parallel_projection = True # or else it does not make sense!
2476            cube = Cube().alpha(0.2)
2477            plt = Plotter(size=(900,600), axes=dict(xtitle='x (um)'))
2478            plt.add_scale_indicator(units='um', c='blue4')
2479            plt.show(cube, "Scale indicator with units").close()
2480            ```
2481            ![](https://vedo.embl.es/images/feats/scale_indicator.png)
2482        """
2483        # Note that this cannot go in addons.py
2484        # because it needs callbacks and window size
2485        if not self.interactor:
2486            return None
2487
2488        ppoints = vtki.vtkPoints()  # Generate the polyline
2489        psqr = [[0.0, gap], [length / 10, gap]]
2490        dd = psqr[1][0] - psqr[0][0]
2491        for i, pt in enumerate(psqr):
2492            ppoints.InsertPoint(i, pt[0], pt[1], 0)
2493        lines = vtki.vtkCellArray()
2494        lines.InsertNextCell(len(psqr))
2495        for i in range(len(psqr)):
2496            lines.InsertCellPoint(i)
2497        pd = vtki.vtkPolyData()
2498        pd.SetPoints(ppoints)
2499        pd.SetLines(lines)
2500
2501        wsx, wsy = self.window.GetSize()
2502        if not self.camera.GetParallelProjection():
2503            vedo.logger.warning("add_scale_indicator called with use_parallel_projection OFF. Skip.")
2504            return None
2505
2506        rlabel = vtki.new("VectorText")
2507        rlabel.SetText("scale")
2508        tf = vtki.new("TransformPolyDataFilter")
2509        tf.SetInputConnection(rlabel.GetOutputPort())
2510        t = vtki.vtkTransform()
2511        t.Scale(s * wsy / wsx, s, 1)
2512        tf.SetTransform(t)
2513
2514        app = vtki.new("AppendPolyData")
2515        app.AddInputConnection(tf.GetOutputPort())
2516        app.AddInputData(pd)
2517
2518        mapper = vtki.new("PolyDataMapper2D")
2519        mapper.SetInputConnection(app.GetOutputPort())
2520        cs = vtki.vtkCoordinate()
2521        cs.SetCoordinateSystem(1)
2522        mapper.SetTransformCoordinate(cs)
2523
2524        fractor = vedo.visual.Actor2D()
2525        csys = fractor.GetPositionCoordinate()
2526        csys.SetCoordinateSystem(3)
2527        fractor.SetPosition(pos)
2528        fractor.SetMapper(mapper)
2529        fractor.GetProperty().SetColor(vedo.get_color(c))
2530        fractor.GetProperty().SetOpacity(alpha)
2531        fractor.GetProperty().SetLineWidth(lw)
2532        fractor.GetProperty().SetDisplayLocationToForeground()
2533
2534        def sifunc(iren, ev):
2535            wsx, wsy = self.window.GetSize()
2536            ps = self.camera.GetParallelScale()
2537            newtxt = utils.precision(ps / wsy * wsx * length * dd, 3)
2538            if units:
2539                newtxt += " " + units
2540            if rlabel.GetText() != newtxt:
2541                rlabel.SetText(newtxt)
2542
2543        self.renderer.AddActor(fractor)
2544        self.interactor.AddObserver("MouseWheelBackwardEvent", sifunc)
2545        self.interactor.AddObserver("MouseWheelForwardEvent", sifunc)
2546        self.interactor.AddObserver("InteractionEvent", sifunc)
2547        sifunc(0, 0)
2548        return fractor
2549
2550    def fill_event(self, ename="", pos=(), enable_picking=True) -> "Event":
2551        """
2552        Create an Event object with information of what was clicked.
2553
2554        If `enable_picking` is False, no picking will be performed.
2555        This can be useful to avoid double picking when using buttons.
2556        """
2557        if not self.interactor:
2558            return Event()
2559
2560        if len(pos) > 0:
2561            x, y = pos
2562            self.interactor.SetEventPosition(pos)
2563        else:
2564            x, y = self.interactor.GetEventPosition()
2565        self.renderer = self.interactor.FindPokedRenderer(x, y)
2566
2567        self.picked2d = (x, y)
2568
2569        key = self.interactor.GetKeySym()
2570
2571        if key:
2572            if "_L" in key or "_R" in key:
2573                # skip things like Shift_R
2574                key = ""  # better than None
2575            else:
2576                if self.interactor.GetShiftKey():
2577                    key = key.upper()
2578
2579                if key == "MINUS":  # fix: vtk9 is ignoring shift chars..
2580                    key = "underscore"
2581                elif key == "EQUAL":  # fix: vtk9 is ignoring shift chars..
2582                    key = "plus"
2583                elif key == "SLASH":  # fix: vtk9 is ignoring shift chars..
2584                    key = "?"
2585
2586                if self.interactor.GetControlKey():
2587                    key = "Ctrl+" + key
2588
2589                if self.interactor.GetAltKey():
2590                    key = "Alt+" + key
2591
2592        if enable_picking:
2593            if not self.picker:
2594                self.picker = vtki.vtkPropPicker()
2595
2596            self.picker.PickProp(x, y, self.renderer)
2597            actor = self.picker.GetProp3D()
2598            # Note that GetProp3D already picks Assembly
2599
2600            xp, yp = self.interactor.GetLastEventPosition()
2601            dx, dy = x - xp, y - yp
2602
2603            delta3d = np.array([0, 0, 0])
2604
2605            if actor:
2606                picked3d = np.array(self.picker.GetPickPosition())
2607
2608                try:
2609                    vobj = actor.retrieve_object()
2610                    old_pt = np.asarray(vobj.picked3d)
2611                    vobj.picked3d = picked3d
2612                    delta3d = picked3d - old_pt
2613                except (AttributeError, TypeError):
2614                    pass
2615
2616            else:
2617                picked3d = None
2618
2619            if not actor:  # try 2D
2620                actor = self.picker.GetActor2D()
2621
2622        event = Event()
2623        event.name = ename
2624        event.title = self.title
2625        event.id = -1  # will be set by the timer wrapper function
2626        event.timerid = -1  # will be set by the timer wrapper function
2627        event.priority = -1  # will be set by the timer wrapper function
2628        event.time = time.time()
2629        event.at = self.renderers.index(self.renderer)
2630        event.keypress = key
2631        if enable_picking:
2632            try:
2633                event.object = actor.retrieve_object()
2634            except AttributeError:
2635                event.object = actor
2636            try:
2637                event.actor = actor.retrieve_object()  # obsolete use object instead
2638            except AttributeError:
2639                event.actor = actor
2640            event.picked3d = picked3d
2641            event.picked2d = (x, y)
2642            event.delta2d = (dx, dy)
2643            event.angle2d = np.arctan2(dy, dx)
2644            event.speed2d = np.sqrt(dx * dx + dy * dy)
2645            event.delta3d = delta3d
2646            event.speed3d = np.sqrt(np.dot(delta3d, delta3d))
2647            event.isPoints = isinstance(event.object, vedo.Points)
2648            event.isMesh = isinstance(event.object, vedo.Mesh)
2649            event.isAssembly = isinstance(event.object, vedo.Assembly)
2650            event.isVolume = isinstance(event.object, vedo.Volume)
2651            event.isImage = isinstance(event.object, vedo.Image)
2652            event.isActor2D = isinstance(event.object, vtki.vtkActor2D)
2653        return event
2654
2655    def add_callback(self, event_name: str, func: Callable, priority=0.0, enable_picking=True) -> int:
2656        """
2657        Add a function to be executed while show() is active.
2658
2659        Return a unique id for the callback.
2660
2661        The callback function (see example below) exposes a dictionary
2662        with the following information:
2663        - `name`: event name,
2664        - `id`: event unique identifier,
2665        - `priority`: event priority (float),
2666        - `interactor`: the interactor object,
2667        - `at`: renderer nr. where the event occurred
2668        - `keypress`: key pressed as string
2669        - `actor`: object picked by the mouse
2670        - `picked3d`: point picked in world coordinates
2671        - `picked2d`: screen coords of the mouse pointer
2672        - `delta2d`: shift wrt previous position (to calculate speed, direction)
2673        - `delta3d`: ...same but in 3D world coords
2674        - `angle2d`: angle of mouse movement on screen
2675        - `speed2d`: speed of mouse movement on screen
2676        - `speed3d`: speed of picked point in world coordinates
2677        - `isPoints`: True if of class
2678        - `isMesh`: True if of class
2679        - `isAssembly`: True if of class
2680        - `isVolume`: True if of class Volume
2681        - `isImage`: True if of class
2682
2683        If `enable_picking` is False, no picking will be performed.
2684        This can be useful to avoid double picking when using buttons.
2685
2686        Frequently used events are:
2687        - `KeyPress`, `KeyRelease`: listen to keyboard events
2688        - `LeftButtonPress`, `LeftButtonRelease`: listen to mouse clicks
2689        - `MiddleButtonPress`, `MiddleButtonRelease`
2690        - `RightButtonPress`, `RightButtonRelease`
2691        - `MouseMove`: listen to mouse pointer changing position
2692        - `MouseWheelForward`, `MouseWheelBackward`
2693        - `Enter`, `Leave`: listen to mouse entering or leaving the window
2694        - `Pick`, `StartPick`, `EndPick`: listen to object picking
2695        - `ResetCamera`, `ResetCameraClippingRange`
2696        - `Error`, `Warning`
2697        - `Char`
2698        - `Timer`
2699
2700        Check the complete list of events [here](https://vtk.org/doc/nightly/html/classvtkCommand.html).
2701
2702        Example:
2703            ```python
2704            from vedo import *
2705
2706            def func(evt):
2707                # this function is called every time the mouse moves
2708                # (evt is a dotted dictionary)
2709                if not evt.object:
2710                    return  # no hit, return
2711                print("point coords =", evt.picked3d)
2712                # print(evt) # full event dump
2713
2714            elli = Ellipsoid()
2715            plt = Plotter(axes=1)
2716            plt.add_callback('mouse hovering', func)
2717            plt.show(elli).close()
2718            ```
2719
2720        Examples:
2721            - [spline_draw1.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/spline_draw1.py)
2722            - [colorlines.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/colorlines.py)
2723
2724                ![](https://vedo.embl.es/images/advanced/spline_draw.png)
2725
2726            - ..and many others!
2727        """
2728        from vtkmodules.util.misc import calldata_type
2729
2730        if not self.interactor:
2731            return 0
2732
2733        if vedo.settings.dry_run_mode >= 1:
2734            return 0
2735
2736        #########################################
2737        @calldata_type(vtki.VTK_INT)
2738        def _func_wrap(iren, ename, timerid=None):
2739            event = self.fill_event(ename=ename, enable_picking=enable_picking)
2740            event.timerid = timerid
2741            event.id = cid
2742            event.priority = priority
2743            self.last_event = event
2744            func(event)
2745
2746        #########################################
2747
2748        event_name = utils.get_vtk_name_event(event_name)
2749
2750        cid = self.interactor.AddObserver(event_name, _func_wrap, priority)
2751        # print(f"Registering event: {event_name} with id={cid}")
2752        return cid
2753
2754    def remove_callback(self, cid: Union[int, str]) -> Self:
2755        """
2756        Remove a callback function by its id
2757        or a whole category of callbacks by their name.
2758
2759        Arguments:
2760            cid : (int, str)
2761                Unique id of the callback.
2762                If an event name is passed all callbacks of that type are removed.
2763        """
2764        if self.interactor:
2765            if isinstance(cid, str):
2766                cid = utils.get_vtk_name_event(cid)
2767                self.interactor.RemoveObservers(cid)
2768            else:
2769                self.interactor.RemoveObserver(cid)
2770        return self
2771
2772    def remove_all_observers(self) -> Self:
2773        """
2774        Remove all observers.
2775
2776        Example:
2777        ```python
2778        from vedo import *
2779
2780        def kfunc(event):
2781            print("Key pressed:", event.keypress)
2782            if event.keypress == 'q':
2783                plt.close()
2784
2785        def rfunc(event):
2786            if event.isImage:
2787                printc("Right-clicked!", event)
2788                plt.render()
2789
2790        img = Image(dataurl+"images/embryo.jpg")
2791
2792        plt = Plotter(size=(1050, 600))
2793        plt.parallel_projection(True)
2794        plt.remove_all_observers()
2795        plt.add_callback("key press", kfunc)
2796        plt.add_callback("mouse right click", rfunc)
2797        plt.show("Right-Click Me! Press q to exit.", img)
2798        plt.close()
2799        ```
2800        """
2801        if self.interactor:
2802            self.interactor.RemoveAllObservers()
2803        return self
2804
2805    def timer_callback(self, action: str, timer_id=None, dt=1, one_shot=False) -> int:
2806        """
2807        Start or stop an existing timer.
2808
2809        Arguments:
2810            action : (str)
2811                Either "create"/"start" or "destroy"/"stop"
2812            timer_id : (int)
2813                When stopping the timer, the ID of the timer as returned when created
2814            dt : (int)
2815                time in milliseconds between each repeated call
2816            one_shot : (bool)
2817                create a one shot timer of prescribed duration instead of a repeating one
2818
2819        Examples:
2820            - [timer_callback1.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/timer_callback1.py)
2821            - [timer_callback2.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/timer_callback2.py)
2822
2823            ![](https://vedo.embl.es/images/advanced/timer_callback1.jpg)
2824        """
2825        if action in ("create", "start"):
2826            if timer_id is not None:
2827                vedo.logger.warning("you set a timer_id but it will be ignored.")
2828            if one_shot:
2829                timer_id = self.interactor.CreateOneShotTimer(dt)
2830            else:
2831                timer_id = self.interactor.CreateRepeatingTimer(dt)
2832            return timer_id
2833
2834        elif action in ("destroy", "stop"):
2835            if timer_id is not None:
2836                self.interactor.DestroyTimer(timer_id)
2837            else:
2838                vedo.logger.warning("please set a timer_id. Cannot stop timer.")
2839        else:
2840            e = f"in timer_callback(). Cannot understand action: {action}\n"
2841            e += " allowed actions are: ['start', 'stop']. Skipped."
2842            vedo.logger.error(e)
2843        return timer_id
2844
2845    def add_observer(self, event_name: str, func: Callable, priority=0.0) -> int:
2846        """
2847        Add a callback function that will be called when an event occurs.
2848        Consider using `add_callback()` instead.
2849        """
2850        if not self.interactor:
2851            return -1
2852        event_name = utils.get_vtk_name_event(event_name)
2853        idd = self.interactor.AddObserver(event_name, func, priority)
2854        return idd
2855
2856    def compute_world_coordinate(
2857        self,
2858        pos2d: MutableSequence[float],
2859        at=None,
2860        objs=(),
2861        bounds=(),
2862        offset=None,
2863        pixeltol=None,
2864        worldtol=None,
2865    ) -> np.ndarray:
2866        """
2867        Transform a 2D point on the screen into a 3D point inside the rendering scene.
2868        If a set of meshes is passed then points are placed onto these.
2869
2870        Arguments:
2871            pos2d : (list)
2872                2D screen coordinates point.
2873            at : (int)
2874                renderer number.
2875            objs : (list)
2876                list of Mesh objects to project the point onto.
2877            bounds : (list)
2878                specify a bounding box as [xmin,xmax, ymin,ymax, zmin,zmax].
2879            offset : (float)
2880                specify an offset value.
2881            pixeltol : (int)
2882                screen tolerance in pixels.
2883            worldtol : (float)
2884                world coordinates tolerance.
2885
2886        Returns:
2887            numpy array, the point in 3D world coordinates.
2888
2889        Examples:
2890            - [cut_freehand.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/cut_freehand.py)
2891            - [mousehover3.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mousehover3.py)
2892
2893            ![](https://vedo.embl.es/images/basic/mousehover3.jpg)
2894        """
2895        if at is not None:
2896            renderer = self.renderers[at]
2897        else:
2898            renderer = self.renderer
2899
2900        if not objs:
2901            pp = vtki.vtkFocalPlanePointPlacer()
2902        else:
2903            pps = vtki.vtkPolygonalSurfacePointPlacer()
2904            for ob in objs:
2905                pps.AddProp(ob.actor)
2906            pp = pps # type: ignore
2907
2908        if len(bounds) == 6:
2909            pp.SetPointBounds(bounds)
2910        if pixeltol:
2911            pp.SetPixelTolerance(pixeltol)
2912        if worldtol:
2913            pp.SetWorldTolerance(worldtol)
2914        if offset:
2915            pp.SetOffset(offset)
2916
2917        worldPos: MutableSequence[float] = [0, 0, 0]
2918        worldOrient: MutableSequence[float] = [0, 0, 0, 0, 0, 0, 0, 0, 0]
2919        pp.ComputeWorldPosition(renderer, pos2d, worldPos, worldOrient)
2920        # validw = pp.ValidateWorldPosition(worldPos, worldOrient)
2921        # validd = pp.ValidateDisplayPosition(renderer, pos2d)
2922        return np.array(worldPos)
2923
2924    def compute_screen_coordinates(self, obj, full_window=False) -> np.ndarray:
2925        """
2926        Given a 3D points in the current renderer (or full window),
2927        find the screen pixel coordinates.
2928
2929        Example:
2930            ```python
2931            from vedo import *
2932
2933            elli = Ellipsoid().point_size(5)
2934
2935            plt = Plotter()
2936            plt.show(elli, "Press q to continue and print the info")
2937
2938            xyscreen = plt.compute_screen_coordinates(elli)
2939            print('xyscreen coords:', xyscreen)
2940
2941            # simulate an event happening at one point
2942            event = plt.fill_event(pos=xyscreen[123])
2943            print(event)
2944            ```
2945        """
2946        try:
2947            obj = obj.vertices
2948        except AttributeError:
2949            pass
2950
2951        if utils.is_sequence(obj):
2952            pts = obj
2953        p2d = []
2954        cs = vtki.vtkCoordinate()
2955        cs.SetCoordinateSystemToWorld()
2956        cs.SetViewport(self.renderer)
2957        for p in pts:
2958            cs.SetValue(p)
2959            if full_window:
2960                p2d.append(cs.GetComputedDisplayValue(self.renderer))
2961            else:
2962                p2d.append(cs.GetComputedViewportValue(self.renderer))
2963        return np.array(p2d, dtype=int)
2964
2965    def pick_area(self, pos1, pos2, at=None) -> "vedo.Mesh":
2966        """
2967        Pick all objects within a box defined by two corner points in 2D screen coordinates.
2968
2969        Returns a frustum Mesh that contains the visible field of view.
2970        This can be used to select objects in a scene or select vertices.
2971
2972        Example:
2973            ```python
2974            from vedo import *
2975
2976            settings.enable_default_mouse_callbacks = False
2977
2978            def mode_select(objs):
2979                print("Selected objects:", objs)
2980                d0 = mode.start_x, mode.start_y # display coords
2981                d1 = mode.end_x, mode.end_y
2982
2983                frustum = plt.pick_area(d0, d1)
2984                col = np.random.randint(0, 10)
2985                infru = frustum.inside_points(mesh)
2986                infru.point_size(10).color(col)
2987                plt.add(frustum, infru).render()
2988
2989            mesh = Mesh(dataurl+"cow.vtk")
2990            mesh.color("k5").linewidth(1)
2991
2992            mode = interactor_modes.BlenderStyle()
2993            mode.callback_select = mode_select
2994
2995            plt = Plotter().user_mode(mode)
2996            plt.show(mesh, axes=1)
2997            ```
2998        """
2999        if at is not None:
3000            ren = self.renderers[at]
3001        else:
3002            ren = self.renderer
3003        area_picker = vtki.vtkAreaPicker()
3004        area_picker.AreaPick(pos1[0], pos1[1], pos2[0], pos2[1], ren)
3005        planes = area_picker.GetFrustum()
3006
3007        fru = vtki.new("FrustumSource")
3008        fru.SetPlanes(planes)
3009        fru.ShowLinesOff()
3010        fru.Update()
3011
3012        afru = vedo.Mesh(fru.GetOutput())
3013        afru.alpha(0.1).lw(1).pickable(False)
3014        afru.name = "Frustum"
3015        return afru
3016
3017    def _scan_input_return_acts(self, objs) -> Any:
3018        # scan the input and return a list of actors
3019        if not utils.is_sequence(objs):
3020            objs = [objs]
3021
3022        #################
3023        wannabe_acts = []
3024        for a in objs:
3025
3026            try:
3027                wannabe_acts.append(a.actor)
3028            except AttributeError:
3029                wannabe_acts.append(a)  # already actor
3030
3031            try:
3032                wannabe_acts.append(a.scalarbar)
3033            except AttributeError:
3034                pass
3035
3036            try:
3037                for sh in a.shadows:
3038                    wannabe_acts.append(sh.actor)
3039            except AttributeError:
3040                pass
3041
3042            try:
3043                wannabe_acts.append(a.trail.actor)
3044                if a.trail.shadows:  # trails may also have shadows
3045                    for sh in a.trail.shadows:
3046                        wannabe_acts.append(sh.actor)
3047            except AttributeError:
3048                pass
3049
3050        #################
3051        scanned_acts = []
3052        for a in wannabe_acts:  # scan content of list
3053
3054            if a is None:
3055                pass
3056
3057            elif isinstance(a, (vtki.vtkActor, vtki.vtkActor2D)):
3058                scanned_acts.append(a)
3059
3060            elif isinstance(a, str):
3061                # assume a 2D comment was given
3062                changed = False  # check if one already exists so to just update text
3063                if self.renderer:  # might be jupyter
3064                    acs = self.renderer.GetActors2D()
3065                    acs.InitTraversal()
3066                    for i in range(acs.GetNumberOfItems()):
3067                        act = acs.GetNextItem()
3068                        if isinstance(act, vedo.shapes.Text2D):
3069                            aposx, aposy = act.GetPosition()
3070                            if aposx < 0.01 and aposy > 0.99:  # "top-left"
3071                                act.text(a)  # update content! no appending nada
3072                                changed = True
3073                                break
3074                    if not changed:
3075                        out = vedo.shapes.Text2D(a)  # append a new one
3076                        scanned_acts.append(out)
3077                # scanned_acts.append(vedo.shapes.Text2D(a)) # naive version
3078
3079            elif isinstance(a, vtki.vtkPolyData):
3080                scanned_acts.append(vedo.Mesh(a).actor)
3081
3082            elif isinstance(a, vtki.vtkImageData):
3083                scanned_acts.append(vedo.Volume(a).actor)
3084
3085            elif isinstance(a, vedo.RectilinearGrid):
3086                scanned_acts.append(a.actor)
3087
3088            elif isinstance(a, vedo.StructuredGrid):
3089                scanned_acts.append(a.actor)
3090
3091            elif isinstance(a, vtki.vtkLight):
3092                scanned_acts.append(a)
3093
3094            elif isinstance(a, vedo.visual.LightKit):
3095                a.lightkit.AddLightsToRenderer(self.renderer)
3096
3097            elif isinstance(a, vtki.get_class("MultiBlockDataSet")):
3098                for i in range(a.GetNumberOfBlocks()):
3099                    b = a.GetBlock(i)
3100                    if isinstance(b, vtki.vtkPolyData):
3101                        scanned_acts.append(vedo.Mesh(b).actor)
3102                    elif isinstance(b, vtki.vtkImageData):
3103                        scanned_acts.append(vedo.Volume(b).actor)
3104
3105            elif isinstance(a, (vtki.vtkProp, vtki.vtkInteractorObserver)):
3106                scanned_acts.append(a)
3107
3108            elif "trimesh" in str(type(a)):
3109                scanned_acts.append(utils.trimesh2vedo(a))
3110
3111            elif "meshlab" in str(type(a)):
3112                if "MeshSet" in str(type(a)):
3113                    for i in range(a.number_meshes()):
3114                        if a.mesh_id_exists(i):
3115                            scanned_acts.append(utils.meshlab2vedo(a.mesh(i)))
3116                else:
3117                    scanned_acts.append(utils.meshlab2vedo(a))
3118
3119            elif "dolfin" in str(type(a)):  # assume a dolfin.Mesh object
3120                import vedo.dolfin as vdlf
3121
3122                scanned_acts.append(vdlf.IMesh(a).actor)
3123
3124            elif "madcad" in str(type(a)):
3125                scanned_acts.append(utils.madcad2vedo(a).actor)
3126
3127            elif "TetgenIO" in str(type(a)):
3128                scanned_acts.append(vedo.TetMesh(a).shrink(0.9).c("pink7").actor)
3129
3130            elif "matplotlib.figure.Figure" in str(type(a)):
3131                scanned_acts.append(vedo.Image(a).clone2d("top-right", 0.6))
3132
3133            else:
3134                vedo.logger.error(f"cannot understand input in show(): {type(a)}")
3135
3136        return scanned_acts
3137
3138    def show(
3139        self,
3140        *objects,
3141        at=None,
3142        axes=None,
3143        resetcam=None,
3144        zoom=False,
3145        interactive=None,
3146        viewup="",
3147        azimuth=0.0,
3148        elevation=0.0,
3149        roll=0.0,
3150        camera=None,
3151        mode=None,
3152        rate=None,
3153        bg=None,
3154        bg2=None,
3155        size=None,
3156        title=None,
3157        screenshot="",
3158    ) -> Any:
3159        """
3160        Render a list of objects.
3161
3162        Arguments:
3163            at : (int)
3164                number of the renderer to plot to, in case of more than one exists
3165
3166            axes : (int)
3167                axis type-1 can be fully customized by passing a dictionary.
3168                Check `addons.Axes()` for the full list of options.
3169                set the type of axes to be shown:
3170                - 0,  no axes
3171                - 1,  draw three gray grid walls
3172                - 2,  show cartesian axes from (0,0,0)
3173                - 3,  show positive range of cartesian axes from (0,0,0)
3174                - 4,  show a triad at bottom left
3175                - 5,  show a cube at bottom left
3176                - 6,  mark the corners of the bounding box
3177                - 7,  draw a 3D ruler at each side of the cartesian axes
3178                - 8,  show the `vtkCubeAxesActor` object
3179                - 9,  show the bounding box outLine
3180                - 10, show three circles representing the maximum bounding box
3181                - 11, show a large grid on the x-y plane
3182                - 12, show polar axes
3183                - 13, draw a simple ruler at the bottom of the window
3184
3185            azimuth/elevation/roll : (float)
3186                move camera accordingly the specified value
3187
3188            viewup: str, list
3189                either `['x', 'y', 'z']` or a vector to set vertical direction
3190
3191            resetcam : (bool)
3192                re-adjust camera position to fit objects
3193
3194            camera : (dict, vtkCamera)
3195                camera parameters can further be specified with a dictionary
3196                assigned to the `camera` keyword (E.g. `show(camera={'pos':(1,2,3), 'thickness':1000,})`):
3197                - pos, `(list)`,  the position of the camera in world coordinates
3198                - focal_point `(list)`, the focal point of the camera in world coordinates
3199                - viewup `(list)`, the view up direction for the camera
3200                - distance `(float)`, set the focal point to the specified distance from the camera position.
3201                - clipping_range `(float)`, distance of the near and far clipping planes along the direction of projection.
3202                - parallel_scale `(float)`, scaling used for a parallel projection, i.e. the height of the viewport
3203                in world-coordinate distances. The default is 1.
3204                Note that the "scale" parameter works as an "inverse scale", larger numbers produce smaller images.
3205                This method has no effect in perspective projection mode.
3206
3207                - thickness `(float)`, set the distance between clipping planes. This method adjusts the far clipping
3208                plane to be set a distance 'thickness' beyond the near clipping plane.
3209
3210                - view_angle `(float)`, the camera view angle, which is the angular height of the camera view
3211                measured in degrees. The default angle is 30 degrees.
3212                This method has no effect in parallel projection mode.
3213                The formula for setting the angle up for perfect perspective viewing is:
3214                angle = 2*atan((h/2)/d) where h is the height of the RenderWindow
3215                (measured by holding a ruler up to your screen) and d is the distance from your eyes to the screen.
3216
3217            interactive : (bool)
3218                pause and interact with window (True) or continue execution (False)
3219
3220            rate : (float)
3221                maximum rate of `show()` in Hertz
3222
3223            mode : (int, str)
3224                set the type of interaction:
3225                - 0 = TrackballCamera [default]
3226                - 1 = TrackballActor
3227                - 2 = JoystickCamera
3228                - 3 = JoystickActor
3229                - 4 = Flight
3230                - 5 = RubberBand2D
3231                - 6 = RubberBand3D
3232                - 7 = RubberBandZoom
3233                - 8 = Terrain
3234                - 9 = Unicam
3235                - 10 = Image
3236                - Check out `vedo.interaction_modes` for more options.
3237
3238            bg : (str, list)
3239                background color in RGB format, or string name
3240
3241            bg2 : (str, list)
3242                second background color to create a gradient background
3243
3244            size : (str, list)
3245                size of the window, e.g. size="fullscreen", or size=[600,400]
3246
3247            title : (str)
3248                window title text
3249
3250            screenshot : (str)
3251                save a screenshot of the window to file
3252        """
3253
3254        if vedo.settings.dry_run_mode >= 2:
3255            return self
3256
3257        if self.wx_widget:
3258            return self
3259
3260        if self.renderers:  # in case of notebooks
3261
3262            if at is None:
3263                at = self.renderers.index(self.renderer)
3264
3265            else:
3266
3267                if at >= len(self.renderers):
3268                    t = f"trying to show(at={at}) but only {len(self.renderers)} renderers exist"
3269                    vedo.logger.error(t)
3270                    return self
3271
3272                self.renderer = self.renderers[at]
3273
3274        if title is not None:
3275            self.title = title
3276
3277        if size is not None:
3278            self.size = size
3279            if self.size[0] == "f":  # full screen
3280                self.size = "fullscreen"
3281                self.window.SetFullScreen(True)
3282                self.window.BordersOn()
3283            else:
3284                self.window.SetSize(int(self.size[0]), int(self.size[1]))
3285
3286        if vedo.settings.default_backend == "vtk":
3287            if str(bg).endswith(".hdr"):
3288                self._add_skybox(bg)
3289            else:
3290                if bg is not None:
3291                    self.backgrcol = vedo.get_color(bg)
3292                    self.renderer.SetBackground(self.backgrcol)
3293                if bg2 is not None:
3294                    self.renderer.GradientBackgroundOn()
3295                    self.renderer.SetBackground2(vedo.get_color(bg2))
3296
3297        if axes is not None:
3298            if isinstance(axes, vedo.Assembly):  # user passing show(..., axes=myaxes)
3299                objects = list(objects)
3300                objects.append(axes)  # move it into the list of normal things to show
3301                axes = 0
3302            self.axes = axes
3303
3304        if interactive is not None:
3305            self._interactive = interactive
3306        if self.offscreen:
3307            self._interactive = False
3308
3309        # camera stuff
3310        if resetcam is not None:
3311            self.resetcam = resetcam
3312
3313        if camera is not None:
3314            self.resetcam = False
3315            viewup = ""
3316            if isinstance(camera, vtki.vtkCamera):
3317                cameracopy = vtki.vtkCamera()
3318                cameracopy.DeepCopy(camera)
3319                self.camera = cameracopy
3320            else:
3321                self.camera = utils.camera_from_dict(camera)
3322
3323        self.add(objects)
3324
3325        # Backend ###############################################################
3326        if vedo.settings.default_backend in ["k3d"]:
3327            return backends.get_notebook_backend(self.objects)
3328        #########################################################################
3329
3330        for ia in utils.flatten(objects):
3331            try:
3332                # fix gray color labels and title to white or black
3333                ltc = np.array(ia.scalarbar.GetLabelTextProperty().GetColor())
3334                if np.linalg.norm(ltc - (0.5, 0.5, 0.5)) / 3 < 0.05:
3335                    c = (0.9, 0.9, 0.9)
3336                    if np.sum(self.renderer.GetBackground()) > 1.5:
3337                        c = (0.1, 0.1, 0.1)
3338                    ia.scalarbar.GetLabelTextProperty().SetColor(c)
3339                    ia.scalarbar.GetTitleTextProperty().SetColor(c)
3340            except AttributeError:
3341                pass
3342
3343        if self.sharecam:
3344            for r in self.renderers:
3345                r.SetActiveCamera(self.camera)
3346
3347        if self.axes is not None:
3348            if viewup != "2d" or self.axes in [1, 8] or isinstance(self.axes, dict):
3349                bns = self.renderer.ComputeVisiblePropBounds()
3350                addons.add_global_axes(self.axes, bounds=bns)
3351
3352        # Backend ###############################################################
3353        if vedo.settings.default_backend in ["ipyvtk", "trame"]:
3354            return backends.get_notebook_backend()
3355        #########################################################################
3356
3357        if self.resetcam:
3358            self.renderer.ResetCamera()
3359
3360        if len(self.renderers) > 1:
3361            self.add_renderer_frame()
3362
3363        if vedo.settings.default_backend == "2d" and not zoom:
3364            zoom = "tightest"
3365
3366        if zoom:
3367            if zoom == "tight":
3368                self.reset_camera(tight=0.04)
3369            elif zoom == "tightest":
3370                self.reset_camera(tight=0.0001)
3371            else:
3372                self.camera.Zoom(zoom)
3373        if elevation:
3374            self.camera.Elevation(elevation)
3375        if azimuth:
3376            self.camera.Azimuth(azimuth)
3377        if roll:
3378            self.camera.Roll(roll)
3379
3380        if len(viewup) > 0:
3381            b = self.renderer.ComputeVisiblePropBounds()
3382            cm = np.array([(b[1] + b[0])/2, (b[3] + b[2])/2, (b[5] + b[4])/2])
3383            sz = np.array([(b[1] - b[0]), (b[3] - b[2]), (b[5] - b[4])])
3384            if viewup == "x":
3385                sz = np.linalg.norm(sz)
3386                self.camera.SetViewUp([1, 0, 0])
3387                self.camera.SetPosition(cm + sz)
3388            elif viewup == "y":
3389                sz = np.linalg.norm(sz)
3390                self.camera.SetViewUp([0, 1, 0])
3391                self.camera.SetPosition(cm + sz)
3392            elif viewup == "z":
3393                sz = np.array([(b[1]-b[0])*0.7, -(b[3]-b[2])*1.0, (b[5]-b[4])*1.2])
3394                self.camera.SetViewUp([0, 0, 1])
3395                self.camera.SetPosition(cm + 2 * sz)
3396            elif utils.is_sequence(viewup):
3397                sz = np.linalg.norm(sz)
3398                self.camera.SetViewUp(viewup)
3399                cpos = np.cross([0, 1, 0], viewup)
3400                self.camera.SetPosition(cm - 2 * sz * cpos)
3401
3402        self.renderer.ResetCameraClippingRange()
3403
3404        self.initialize_interactor()
3405
3406        if vedo.settings.immediate_rendering:
3407            self.window.Render()  ##################### <-------------- Render
3408
3409        if self.interactor:  # can be offscreen or not the vtk backend..
3410
3411            self.window.SetWindowName(self.title)
3412
3413            # pic = vedo.Image(vedo.dataurl+'images/vtk_logo.png')
3414            # pic = vedo.Image('/home/musy/Downloads/icons8-3d-96.png')
3415            # print(pic.dataset)# Array 0 name PNGImage
3416            # self.window.SetIcon(pic.dataset)
3417
3418            try:
3419                # Needs "pip install pyobjc" on Mac OSX
3420                if (
3421                    self._cocoa_initialized is False
3422                    and "Darwin" in vedo.sys_platform
3423                    and not self.offscreen
3424                ):
3425                    self._cocoa_initialized = True
3426                    from Cocoa import NSRunningApplication, NSApplicationActivateIgnoringOtherApps # type: ignore
3427                    pid = os.getpid()
3428                    x = NSRunningApplication.runningApplicationWithProcessIdentifier_(int(pid))
3429                    x.activateWithOptions_(NSApplicationActivateIgnoringOtherApps)
3430            except:
3431                # vedo.logger.debug("On Mac OSX try: pip install pyobjc")
3432                pass
3433
3434            # Set the interaction style
3435            if mode is not None:
3436                self.user_mode(mode)
3437            if self.qt_widget and mode is None:
3438                self.user_mode(0)
3439
3440            if screenshot:
3441                self.screenshot(screenshot)
3442
3443            if self._interactive:
3444                self.interactor.Start()
3445                if self._must_close_now:
3446                    self.interactor.GetRenderWindow().Finalize()
3447                    self.interactor.TerminateApp()
3448                    self.camera = None
3449                    self.renderer = None
3450                    self.renderers = []
3451                    self.window = None
3452                    self.interactor = None
3453                return self
3454
3455            if rate:
3456                if self.clock is None:  # set clock and limit rate
3457                    self._clockt0 = time.time()
3458                    self.clock = 0.0
3459                else:
3460                    t = time.time() - self._clockt0
3461                    elapsed = t - self.clock
3462                    mint = 1.0 / rate
3463                    if elapsed < mint:
3464                        time.sleep(mint - elapsed)
3465                    self.clock = time.time() - self._clockt0
3466
3467        # 2d ####################################################################
3468        if vedo.settings.default_backend == "2d":
3469            return backends.get_notebook_backend()
3470        #########################################################################
3471
3472        return self
3473
3474
3475    def add_inset(self, *objects, **options) -> Union[vtki.vtkOrientationMarkerWidget, None]:
3476        """Add a draggable inset space into a renderer.
3477
3478        Arguments:
3479            at : (int)
3480                specify the renderer number
3481            pos : (list)
3482                icon position in the range [1-4] indicating one of the 4 corners,
3483                or it can be a tuple (x,y) as a fraction of the renderer size.
3484            size : (float)
3485                size of the square inset
3486            draggable : (bool)
3487                if True the subrenderer space can be dragged around
3488            c : (color)
3489                color of the inset frame when dragged
3490
3491        Examples:
3492            - [inset.py](https://github.com/marcomusy/vedo/tree/master/examples/other/inset.py)
3493
3494            ![](https://user-images.githubusercontent.com/32848391/56758560-3c3f1300-6797-11e9-9b33-49f5a4876039.jpg)
3495        """
3496        if not self.interactor:
3497            return None
3498
3499        if not self.renderer:
3500            vedo.logger.warning("call add_inset() only after first rendering of the scene.")
3501            return None
3502
3503        options = dict(options)
3504        pos = options.pop("pos", 0)
3505        size = options.pop("size", 0.1)
3506        c = options.pop("c", "lb")
3507        at = options.pop("at", None)
3508        draggable = options.pop("draggable", True)
3509
3510        r, g, b = vedo.get_color(c)
3511        widget = vtki.vtkOrientationMarkerWidget()
3512        widget.SetOutlineColor(r, g, b)
3513        if len(objects) == 1:
3514            widget.SetOrientationMarker(objects[0].actor)
3515        else:
3516            widget.SetOrientationMarker(vedo.Assembly(objects))
3517
3518        widget.SetInteractor(self.interactor)
3519
3520        if utils.is_sequence(pos):
3521            widget.SetViewport(pos[0] - size, pos[1] - size, pos[0] + size, pos[1] + size)
3522        else:
3523            if pos < 2:
3524                widget.SetViewport(0, 1 - 2 * size, size * 2, 1)
3525            elif pos == 2:
3526                widget.SetViewport(1 - 2 * size, 1 - 2 * size, 1, 1)
3527            elif pos == 3:
3528                widget.SetViewport(0, 0, size * 2, size * 2)
3529            elif pos == 4:
3530                widget.SetViewport(1 - 2 * size, 0, 1, size * 2)
3531        widget.EnabledOn()
3532        widget.SetInteractive(draggable)
3533        if at is not None and at < len(self.renderers):
3534            widget.SetCurrentRenderer(self.renderers[at])
3535        else:
3536            widget.SetCurrentRenderer(self.renderer)
3537        self.widgets.append(widget)
3538        return widget
3539
3540    def clear(self, at=None, deep=False) -> Self:
3541        """Clear the scene from all meshes and volumes."""
3542        if at is not None:
3543            renderer = self.renderers[at]
3544        else:
3545            renderer = self.renderer
3546        if not renderer:
3547            return self
3548
3549        if deep:
3550            renderer.RemoveAllViewProps()
3551        else:
3552            for ob in set(
3553                self.get_meshes()
3554                + self.get_volumes()
3555                + self.objects
3556                + self.axes_instances
3557            ):
3558                if isinstance(ob, vedo.shapes.Text2D):
3559                    continue
3560                self.remove(ob)
3561                try:
3562                    if ob.scalarbar:
3563                        self.remove(ob.scalarbar)
3564                except AttributeError:
3565                    pass
3566        return self
3567
3568    def break_interaction(self) -> Self:
3569        """Break window interaction and return to the python execution flow"""
3570        if self.interactor:
3571            self.check_actors_trasform()
3572            self.interactor.ExitCallback()
3573        return self
3574
3575    def freeze(self, value=True) -> Self:
3576        """Freeze the current renderer. Use this with `sharecam=False`."""
3577        if not self.interactor:
3578            return self
3579        if not self.renderer:
3580            return self
3581        self.renderer.SetInteractive(not value)
3582        return self
3583
3584    def user_mode(self, mode) -> Self:
3585        """
3586        Modify the user interaction mode.
3587
3588        Examples:
3589            ```python
3590            from vedo import *
3591            mode = interactor_modes.MousePan()
3592            mesh = Mesh(dataurl+"cow.vtk")
3593            plt = Plotter().user_mode(mode)
3594            plt.show(mesh, axes=1)
3595           ```
3596        See also:
3597        [VTK interactor styles](https://vtk.org/doc/nightly/html/classvtkInteractorStyle.html)
3598        """
3599        if not self.interactor:
3600            return self
3601        
3602        curr_style = self.interactor.GetInteractorStyle().GetClassName()
3603        # print("Current style:", curr_style)
3604        if curr_style.endswith("Actor"):
3605            self.check_actors_trasform()
3606
3607        if isinstance(mode, (str, int)):
3608            # Set the style of interaction
3609            # see https://vtk.org/doc/nightly/html/classvtkInteractorStyle.html
3610            if   mode in (0, "TrackballCamera"):
3611                self.interactor.SetInteractorStyle(vtki.new("InteractorStyleTrackballCamera"))
3612                self.interactor.RemoveObservers("CharEvent")
3613            elif mode in (1, "TrackballActor"):
3614                self.interactor.SetInteractorStyle(vtki.new("InteractorStyleTrackballActor"))
3615            elif mode in (2, "JoystickCamera"):
3616                self.interactor.SetInteractorStyle(vtki.new("InteractorStyleJoystickCamera"))
3617            elif mode in (3, "JoystickActor"):
3618                self.interactor.SetInteractorStyle(vtki.new("InteractorStyleJoystickActor"))
3619            elif mode in (4, "Flight"):
3620                self.interactor.SetInteractorStyle(vtki.new("InteractorStyleFlight"))
3621            elif mode in (5, "RubberBand2D"):
3622                self.interactor.SetInteractorStyle(vtki.new("InteractorStyleRubberBand2D"))
3623            elif mode in (6, "RubberBand3D"):
3624                self.interactor.SetInteractorStyle(vtki.new("InteractorStyleRubberBand3D"))
3625            elif mode in (7, "RubberBandZoom"):
3626                self.interactor.SetInteractorStyle(vtki.new("InteractorStyleRubberBandZoom"))
3627            elif mode in (8, "Terrain"):
3628                self.interactor.SetInteractorStyle(vtki.new("InteractorStyleTerrain"))
3629            elif mode in (9, "Unicam"):
3630                self.interactor.SetInteractorStyle(vtki.new("InteractorStyleUnicam"))
3631            elif mode in (10, "Image", "image", "2d"):
3632                astyle = vtki.new("InteractorStyleImage")
3633                astyle.SetInteractionModeToImage3D()
3634                self.interactor.SetInteractorStyle(astyle)
3635            else:
3636                vedo.logger.warning(f"Unknown interaction mode: {mode}")
3637
3638        elif isinstance(mode, vtki.vtkInteractorStyleUser):
3639            # set a custom interactor style
3640            if hasattr(mode, "interactor"):
3641                mode.interactor = self.interactor
3642                mode.renderer = self.renderer # type: ignore
3643            mode.SetInteractor(self.interactor)
3644            mode.SetDefaultRenderer(self.renderer)
3645            self.interactor.SetInteractorStyle(mode)
3646
3647        return self
3648
3649    def close(self) -> Self:
3650        """Close the plotter."""
3651        # https://examples.vtk.org/site/Cxx/Visualization/CloseWindow/
3652        vedo.last_figure = None
3653        self.last_event = None
3654        self.sliders = []
3655        self.buttons = []
3656        self.widgets = []
3657        self.hover_legends = []
3658        self.background_renderer = None
3659        self._extralight = None
3660
3661        self.hint_widget = None
3662        self.cutter_widget = None
3663
3664        if vedo.settings.dry_run_mode >= 2:
3665            return self
3666        
3667        if not hasattr(self, "window"):
3668            return self
3669        if not self.window:
3670            return self
3671        if not hasattr(self, "interactor"):
3672            return self
3673        if not self.interactor:
3674            return self
3675
3676        ###################################################
3677        try:
3678            if "Darwin" in vedo.sys_platform:
3679                self.interactor.ProcessEvents()
3680        except:
3681            pass
3682
3683        self._must_close_now = True
3684
3685        if vedo.plotter_instance == self:
3686            vedo.plotter_instance = None
3687
3688        if self.interactor and self._interactive:
3689            self.break_interaction()
3690        elif self._must_close_now:
3691            # dont call ExitCallback here
3692            if self.interactor:
3693                self.break_interaction()
3694                self.interactor.GetRenderWindow().Finalize()
3695                self.interactor.TerminateApp()
3696            self.camera = None
3697            self.renderer = None
3698            self.renderers = []
3699            self.window = None
3700            self.interactor = None
3701        return self
3702
3703    @property
3704    def camera(self):
3705        """Return the current active camera."""
3706        if self.renderer:
3707            return self.renderer.GetActiveCamera()
3708
3709    @camera.setter
3710    def camera(self, cam):
3711        if self.renderer:
3712            if isinstance(cam, dict):
3713                cam = utils.camera_from_dict(cam)
3714            self.renderer.SetActiveCamera(cam)
3715
3716    def screenshot(self, filename="screenshot.png", scale=1, asarray=False) -> Any:
3717        """
3718        Take a screenshot of the Plotter window.
3719
3720        Arguments:
3721            scale : (int)
3722                set image magnification as an integer multiplicating factor
3723            asarray : (bool)
3724                return a numpy array of the image instead of writing a file
3725
3726        Warning:
3727            If you get black screenshots try to set `interactive=False` in `show()`
3728            then call `screenshot()` and `plt.interactive()` afterwards.
3729
3730        Example:
3731            ```py
3732            from vedo import *
3733            sphere = Sphere().linewidth(1)
3734            plt = show(sphere, interactive=False)
3735            plt.screenshot('image.png')
3736            plt.interactive()
3737            plt.close()
3738            ```
3739
3740        Example:
3741            ```py
3742            from vedo import *
3743            sphere = Sphere().linewidth(1)
3744            plt = show(sphere, interactive=False)
3745            plt.screenshot('anotherimage.png')
3746            plt.interactive()
3747            plt.close()
3748            ```
3749        """
3750        return vedo.file_io.screenshot(filename, scale, asarray)
3751
3752    def toimage(self, scale=1) -> "vedo.image.Image":
3753        """
3754        Generate a `Image` object from the current rendering window.
3755
3756        Arguments:
3757            scale : (int)
3758                set image magnification as an integer multiplicating factor
3759        """
3760        if vedo.settings.screeshot_large_image:
3761            w2if = vtki.new("RenderLargeImage")
3762            w2if.SetInput(self.renderer)
3763            w2if.SetMagnification(scale)
3764        else:
3765            w2if = vtki.new("WindowToImageFilter")
3766            w2if.SetInput(self.window)
3767            if hasattr(w2if, "SetScale"):
3768                w2if.SetScale(scale, scale)
3769            if vedo.settings.screenshot_transparent_background:
3770                w2if.SetInputBufferTypeToRGBA()
3771            w2if.ReadFrontBufferOff()  # read from the back buffer
3772        w2if.Update()
3773        return vedo.image.Image(w2if.GetOutput())
3774
3775    def export(self, filename="scene.npz", binary=False) -> Self:
3776        """
3777        Export scene to file to HTML, X3D or Numpy file.
3778
3779        Examples:
3780            - [export_x3d.py](https://github.com/marcomusy/vedo/tree/master/examples/other/export_x3d.py)
3781            - [export_numpy.py](https://github.com/marcomusy/vedo/tree/master/examples/other/export_numpy.py)
3782        """
3783        vedo.file_io.export_window(filename, binary=binary)
3784        return self
3785
3786    def color_picker(self, xy, verbose=False):
3787        """Pick color of specific (x,y) pixel on the screen."""
3788        w2if = vtki.new("WindowToImageFilter")
3789        w2if.SetInput(self.window)
3790        w2if.ReadFrontBufferOff()
3791        w2if.Update()
3792        nx, ny = self.window.GetSize()
3793        varr = w2if.GetOutput().GetPointData().GetScalars()
3794
3795        arr = utils.vtk2numpy(varr).reshape(ny, nx, 3)
3796        x, y = int(xy[0]), int(xy[1])
3797        if y < ny and x < nx:
3798
3799            rgb = arr[y, x]
3800
3801            if verbose:
3802                vedo.printc(":rainbow:Pixel", [x, y], "has RGB[", end="")
3803                vedo.printc("â–ˆ", c=[rgb[0], 0, 0], end="")
3804                vedo.printc("â–ˆ", c=[0, rgb[1], 0], end="")
3805                vedo.printc("â–ˆ", c=[0, 0, rgb[2]], end="")
3806                vedo.printc("] = ", end="")
3807                cnm = vedo.get_color_name(rgb)
3808                if np.sum(rgb) < 150:
3809                    vedo.printc(
3810                        rgb.tolist(),
3811                        vedo.colors.rgb2hex(np.array(rgb) / 255),
3812                        c="w",
3813                        bc=rgb,
3814                        invert=1,
3815                        end="",
3816                    )
3817                    vedo.printc("  -> " + cnm, invert=1, c="w")
3818                else:
3819                    vedo.printc(
3820                        rgb.tolist(),
3821                        vedo.colors.rgb2hex(np.array(rgb) / 255),
3822                        c=rgb,
3823                        end="",
3824                    )
3825                    vedo.printc("  -> " + cnm, c=cnm)
3826
3827            return rgb
3828
3829        return None
3830
3831    #######################################################################
3832    def _default_mouseleftclick(self, iren, event) -> None:
3833        x, y = iren.GetEventPosition()
3834        renderer = iren.FindPokedRenderer(x, y)
3835        picker = vtki.vtkPropPicker()
3836        picker.PickProp(x, y, renderer)
3837
3838        self.renderer = renderer
3839
3840        clicked_actor = picker.GetActor()
3841        # clicked_actor2D = picker.GetActor2D()
3842
3843        # print('_default_mouseleftclick mouse at', x, y)
3844        # print("picked Volume:",   [picker.GetVolume()])
3845        # print("picked Actor2D:",  [picker.GetActor2D()])
3846        # print("picked Assembly:", [picker.GetAssembly()])
3847        # print("picked Prop3D:",   [picker.GetProp3D()])
3848
3849        if not clicked_actor:
3850            clicked_actor = picker.GetAssembly()
3851
3852        if not clicked_actor:
3853            clicked_actor = picker.GetProp3D()
3854
3855        if not hasattr(clicked_actor, "GetPickable") or not clicked_actor.GetPickable():
3856            return
3857
3858        self.picked3d = picker.GetPickPosition()
3859        self.picked2d = np.array([x, y])
3860
3861        if not clicked_actor:
3862            return
3863
3864        self.justremoved = None
3865        self.clicked_actor = clicked_actor
3866
3867        try:  # might not be a vedo obj
3868            self.clicked_object = clicked_actor.retrieve_object()
3869            # save this info in the object itself
3870            self.clicked_object.picked3d = self.picked3d
3871            self.clicked_object.picked2d = self.picked2d
3872        except AttributeError:
3873            pass
3874
3875        # -----------
3876        # if "Histogram1D" in picker.GetAssembly().__class__.__name__:
3877        #     histo = picker.GetAssembly()
3878        #     if histo.verbose:
3879        #         x = self.picked3d[0]
3880        #         idx = np.digitize(x, histo.edges) - 1
3881        #         f = histo.frequencies[idx]
3882        #         cn = histo.centers[idx]
3883        #         vedo.colors.printc(f"{histo.name}, bin={idx}, center={cn}, value={f}")
3884
3885    #######################################################################
3886    def _default_keypress(self, iren, event) -> None:
3887        # NB: qt creates and passes a vtkGenericRenderWindowInteractor
3888
3889        key = iren.GetKeySym()
3890
3891        if "_L" in key or "_R" in key:
3892            return
3893
3894        if iren.GetShiftKey():
3895            key = key.upper()
3896
3897        if iren.GetControlKey():
3898            key = "Ctrl+" + key
3899
3900        if iren.GetAltKey():
3901            key = "Alt+" + key
3902
3903        #######################################################
3904        # utils.vedo.printc('Pressed key:', key, c='y', box='-')
3905        # print(key, iren.GetShiftKey(), iren.GetAltKey(), iren.GetControlKey(),
3906        #       iren.GetKeyCode(), iren.GetRepeatCount())
3907        #######################################################
3908
3909        x, y = iren.GetEventPosition()
3910        renderer = iren.FindPokedRenderer(x, y)
3911
3912        if key in ["q", "Return"]:
3913            self.break_interaction()
3914            return
3915
3916        elif key in ["Ctrl+q", "Ctrl+w", "Escape"]:
3917            self.close()
3918            return
3919
3920        elif key == "F1":
3921            vedo.logger.info("Execution aborted. Exiting python kernel now.")
3922            self.break_interaction()
3923            sys.exit(0)
3924
3925        elif key == "Down":
3926            if self.clicked_object and self.clicked_object in self.get_meshes():
3927                self.clicked_object.alpha(0.02)
3928                if hasattr(self.clicked_object, "properties_backface"):
3929                    bfp = self.clicked_actor.GetBackfaceProperty()
3930                    self.clicked_object.properties_backface = bfp  # save it
3931                    self.clicked_actor.SetBackfaceProperty(None)
3932            else:
3933                for obj in self.get_meshes():
3934                    if obj:
3935                        obj.alpha(0.02)
3936                        bfp = obj.actor.GetBackfaceProperty()
3937                        if bfp and hasattr(obj, "properties_backface"):
3938                            obj.properties_backface = bfp
3939                            obj.actor.SetBackfaceProperty(None)
3940
3941        elif key == "Left":
3942            if self.clicked_object and self.clicked_object in self.get_meshes():
3943                ap = self.clicked_object.properties
3944                aal = max([ap.GetOpacity() * 0.75, 0.01])
3945                ap.SetOpacity(aal)
3946                bfp = self.clicked_actor.GetBackfaceProperty()
3947                if bfp and hasattr(self.clicked_object, "properties_backface"):
3948                    self.clicked_object.properties_backface = bfp
3949                    self.clicked_actor.SetBackfaceProperty(None)
3950            else:
3951                for a in self.get_meshes():
3952                    if a:
3953                        ap = a.properties
3954                        aal = max([ap.GetOpacity() * 0.75, 0.01])
3955                        ap.SetOpacity(aal)
3956                        bfp = a.actor.GetBackfaceProperty()
3957                        if bfp and hasattr(a, "properties_backface"):
3958                            a.properties_backface = bfp
3959                            a.actor.SetBackfaceProperty(None)
3960
3961        elif key == "Right":
3962            if self.clicked_object and self.clicked_object in self.get_meshes():
3963                ap = self.clicked_object.properties
3964                aal = min([ap.GetOpacity() * 1.25, 1.0])
3965                ap.SetOpacity(aal)
3966                if (
3967                    aal == 1
3968                    and hasattr(self.clicked_object, "properties_backface")
3969                    and self.clicked_object.properties_backface
3970                ):
3971                    # put back
3972                    self.clicked_actor.SetBackfaceProperty(
3973                        self.clicked_object.properties_backface)
3974            else:
3975                for a in self.get_meshes():
3976                    if a:
3977                        ap = a.properties
3978                        aal = min([ap.GetOpacity() * 1.25, 1.0])
3979                        ap.SetOpacity(aal)
3980                        if aal == 1 and hasattr(a, "properties_backface") and a.properties_backface:
3981                            a.actor.SetBackfaceProperty(a.properties_backface)
3982
3983        elif key == "Up":
3984            if self.clicked_object and self.clicked_object in self.get_meshes():
3985                self.clicked_object.properties.SetOpacity(1)
3986                if hasattr(self.clicked_object, "properties_backface") and self.clicked_object.properties_backface:
3987                    self.clicked_object.actor.SetBackfaceProperty(self.clicked_object.properties_backface)
3988            else:
3989                for a in self.get_meshes():
3990                    if a:
3991                        a.properties.SetOpacity(1)
3992                        if hasattr(a, "properties_backface") and a.properties_backface:
3993                            a.actor.SetBackfaceProperty(a.properties_backface)
3994
3995        elif key == "P":
3996            if self.clicked_object and self.clicked_object in self.get_meshes():
3997                objs = [self.clicked_object]
3998            else:
3999                objs = self.get_meshes()
4000            for ia in objs:
4001                try:
4002                    ps = ia.properties.GetPointSize()
4003                    if ps > 1:
4004                        ia.properties.SetPointSize(ps - 1)
4005                    ia.properties.SetRepresentationToPoints()
4006                except AttributeError:
4007                    pass
4008
4009        elif key == "p":
4010            if self.clicked_object and self.clicked_object in self.get_meshes():
4011                objs = [self.clicked_object]
4012            else:
4013                objs = self.get_meshes()
4014            for ia in objs:
4015                try:
4016                    ps = ia.properties.GetPointSize()
4017                    ia.properties.SetPointSize(ps + 2)
4018                    ia.properties.SetRepresentationToPoints()
4019                except AttributeError:
4020                    pass
4021
4022        elif key == "U":
4023            pval = renderer.GetActiveCamera().GetParallelProjection()
4024            renderer.GetActiveCamera().SetParallelProjection(not pval)
4025            if pval:
4026                renderer.ResetCamera()
4027
4028        elif key == "r":
4029            renderer.ResetCamera()
4030
4031        elif key == "h":
4032            msg  = f" vedo {vedo.__version__}"
4033            msg += f" | vtk {vtki.vtkVersion().GetVTKVersion()}"
4034            msg += f" | numpy {np.__version__}"
4035            msg += f" | python {sys.version_info[0]}.{sys.version_info[1]}, press: "
4036            vedo.printc(msg.ljust(75), invert=True)
4037            msg = (
4038                "    i     print info about the last clicked object     \n"
4039                "    I     print color of the pixel under the mouse     \n"
4040                "    Y     show the pipeline for this object as a graph \n"
4041                "    <- -> use arrows to reduce/increase opacity        \n"
4042                "    x     toggle mesh visibility                       \n"
4043                "    w     toggle wireframe/surface style               \n"
4044                "    l     toggle surface edges visibility              \n"
4045                "    p/P   hide surface faces and show only points      \n"
4046                "    1-3   cycle surface color (2=light, 3=dark)        \n"
4047                "    4     cycle color map (press shift-4 to go back)   \n"
4048                "    5-6   cycle point-cell arrays (shift to go back)   \n"
4049                "    7-8   cycle background and gradient color          \n"
4050                "    09+-  cycle axes styles (on keypad, or press +/-)  \n"
4051                "    k     cycle available lighting styles              \n"
4052                "    K     toggle shading as flat or phong              \n"
4053                "    A     toggle anti-aliasing                         \n"
4054                "    D     toggle depth-peeling (for transparencies)    \n"
4055                "    U     toggle perspective/parallel projection       \n"
4056                "    o/O   toggle extra light to scene and rotate it    \n"
4057                "    a     toggle interaction to Actor Mode             \n"
4058                "    n     toggle surface normals                       \n"
4059                "    r     reset camera position                        \n"
4060                "    R     reset camera to the closest orthogonal view  \n"
4061                "    .     fly camera to the last clicked point         \n"
4062                "    C     print the current camera parameters state    \n"
4063                "    X     invoke a cutter widget tool                  \n"
4064                "    S     save a screenshot of the current scene       \n"
4065                "    E/F   export 3D scene to numpy file or X3D         \n"
4066                "    q     return control to python script              \n"
4067                "    Esc   abort execution and exit python kernel       "
4068            )
4069            vedo.printc(msg, dim=True, italic=True, bold=True)
4070            vedo.printc(
4071                " Check out the documentation at:  https://vedo.embl.es ".ljust(75),
4072                invert=True,
4073                bold=True,
4074            )
4075            return
4076
4077        elif key == "a":
4078            cur = iren.GetInteractorStyle()
4079            if isinstance(cur, vtki.get_class("InteractorStyleTrackballCamera")):
4080                msg  = "Interactor style changed to TrackballActor\n"
4081                msg += "  you can now move and rotate individual meshes:\n"
4082                msg += "  press X twice to save the repositioned mesh\n"
4083                msg += "  press 'a' to go back to normal style"
4084                vedo.printc(msg)
4085                iren.SetInteractorStyle(vtki.new("InteractorStyleTrackballActor"))
4086            else:
4087                iren.SetInteractorStyle(vtki.new("InteractorStyleTrackballCamera"))
4088            return
4089
4090        elif key == "A":  # toggle antialiasing
4091            msam = self.window.GetMultiSamples()
4092            if not msam:
4093                self.window.SetMultiSamples(16)
4094            else:
4095                self.window.SetMultiSamples(0)
4096            msam = self.window.GetMultiSamples()
4097            if msam:
4098                vedo.printc(f"Antialiasing set to {msam} samples", c=bool(msam))
4099            else:
4100                vedo.printc("Antialiasing disabled", c=bool(msam))
4101
4102        elif key == "D":  # toggle depthpeeling
4103            udp = not renderer.GetUseDepthPeeling()
4104            renderer.SetUseDepthPeeling(udp)
4105            # self.renderer.SetUseDepthPeelingForVolumes(udp)
4106            if udp:
4107                self.window.SetAlphaBitPlanes(1)
4108                renderer.SetMaximumNumberOfPeels(vedo.settings.max_number_of_peels)
4109                renderer.SetOcclusionRatio(vedo.settings.occlusion_ratio)
4110            self.interactor.Render()
4111            wasUsed = renderer.GetLastRenderingUsedDepthPeeling()
4112            rnr = self.renderers.index(renderer)
4113            vedo.printc(f"Depth peeling set to {udp} for renderer nr.{rnr}", c=udp)
4114            if not wasUsed and udp:
4115                vedo.printc("\t...but last rendering did not actually used it!", c=udp, invert=True)
4116            return
4117
4118        elif key == "period":
4119            if self.picked3d:
4120                self.fly_to(self.picked3d)
4121            return
4122
4123        elif key == "S":
4124            fname = "screenshot.png"
4125            i = 1
4126            while os.path.isfile(fname):
4127                fname = f"screenshot{i}.png"
4128                i += 1
4129            vedo.file_io.screenshot(fname)
4130            vedo.printc(rf":camera: Saved rendering window to {fname}", c="b")
4131            return
4132
4133        elif key == "C":
4134            # Precision needs to be 7 (or even larger) to guarantee a consistent camera when
4135            #   the model coordinates are not centered at (0, 0, 0) and the mode is large.
4136            # This could happen for plotting geological models with UTM coordinate systems
4137            cam = renderer.GetActiveCamera()
4138            vedo.printc("\n###################################################", c="y")
4139            vedo.printc("## Template python code to position this camera: ##", c="y")
4140            vedo.printc("cam = dict(", c="y")
4141            vedo.printc("    pos=" + utils.precision(cam.GetPosition(), 6) + ",", c="y")
4142            vedo.printc("    focal_point=" + utils.precision(cam.GetFocalPoint(), 6) + ",", c="y")
4143            vedo.printc("    viewup=" + utils.precision(cam.GetViewUp(), 6) + ",", c="y")
4144            vedo.printc("    roll=" + utils.precision(cam.GetRoll(), 6) + ",", c="y")
4145            if cam.GetParallelProjection():
4146                vedo.printc('    parallel_scale='+utils.precision(cam.GetParallelScale(),6)+',', c='y')
4147            else:
4148                vedo.printc('    distance='     +utils.precision(cam.GetDistance(),6)+',', c='y')
4149            vedo.printc('    clipping_range='+utils.precision(cam.GetClippingRange(),6)+',', c='y')
4150            vedo.printc(')', c='y')
4151            vedo.printc('show(mymeshes, camera=cam)', c='y')
4152            vedo.printc('###################################################', c='y')
4153            return
4154
4155        elif key == "R":
4156            self.reset_viewup()
4157
4158        elif key == "w":
4159            try:
4160                if self.clicked_object.properties.GetRepresentation() == 1:  # toggle
4161                    self.clicked_object.properties.SetRepresentationToSurface()
4162                else:
4163                    self.clicked_object.properties.SetRepresentationToWireframe()
4164            except AttributeError:
4165                pass
4166
4167        elif key == "1":
4168            try:
4169                self._icol += 1
4170                self.clicked_object.mapper.ScalarVisibilityOff()
4171                pal = vedo.colors.palettes[vedo.settings.palette % len(vedo.colors.palettes)]
4172                self.clicked_object.c(pal[(self._icol) % 10])
4173                self.remove(self.clicked_object.scalarbar)
4174            except AttributeError:
4175                pass
4176
4177        elif key == "2": # dark colors
4178            try:
4179                bsc = ["k1", "k2", "k3", "k4",
4180                    "b1", "b2", "b3", "b4",
4181                    "p1", "p2", "p3", "p4",
4182                    "g1", "g2", "g3", "g4",
4183                    "r1", "r2", "r3", "r4",
4184                    "o1", "o2", "o3", "o4",
4185                    "y1", "y2", "y3", "y4"]
4186                self._icol += 1
4187                if self.clicked_object:
4188                    self.clicked_object.mapper.ScalarVisibilityOff()
4189                    newcol = vedo.get_color(bsc[(self._icol) % len(bsc)])
4190                    self.clicked_object.c(newcol)
4191                    self.remove(self.clicked_object.scalarbar)
4192            except AttributeError:
4193                pass
4194
4195        elif key == "3": # light colors
4196            try:
4197                bsc = ["k6", "k7", "k8", "k9",
4198                    "b6", "b7", "b8", "b9",
4199                    "p6", "p7", "p8", "p9",
4200                    "g6", "g7", "g8", "g9",
4201                    "r6", "r7", "r8", "r9",
4202                    "o6", "o7", "o8", "o9",
4203                    "y6", "y7", "y8", "y9"]
4204                self._icol += 1
4205                if self.clicked_object:
4206                    self.clicked_object.mapper.ScalarVisibilityOff()
4207                    newcol = vedo.get_color(bsc[(self._icol) % len(bsc)])
4208                    self.clicked_object.c(newcol)
4209                    self.remove(self.clicked_object.scalarbar)
4210            except AttributeError:
4211                pass
4212
4213        elif key == "4":  # cmap name cycle
4214            ob = self.clicked_object
4215            if not isinstance(ob, (vedo.Points, vedo.UnstructuredGrid)):
4216                return
4217            if not ob.mapper.GetScalarVisibility():
4218                return
4219            onwhat = ob.mapper.GetScalarModeAsString()  # UsePointData/UseCellData
4220
4221            cmap_names = [
4222                "Accent", "Paired",
4223                "rainbow", "rainbow_r",
4224                "Spectral", "Spectral_r",
4225                "gist_ncar", "gist_ncar_r",
4226                "viridis", "viridis_r",
4227                "hot", "hot_r",
4228                "terrain", "ocean",
4229                "coolwarm", "seismic", "PuOr", "RdYlGn",
4230            ]
4231            try:
4232                i = cmap_names.index(ob._cmap_name)
4233                if iren.GetShiftKey():
4234                    i -= 1
4235                else:
4236                    i += 1
4237                if i >= len(cmap_names):
4238                    i = 0
4239                if i < 0:
4240                    i = len(cmap_names) - 1
4241            except ValueError:
4242                i = 0
4243
4244            ob._cmap_name = cmap_names[i]
4245            ob.cmap(ob._cmap_name, on=onwhat)
4246            if ob.scalarbar:
4247                if isinstance(ob.scalarbar, vtki.vtkActor2D):
4248                    self.remove(ob.scalarbar)
4249                    title = ob.scalarbar.GetTitle()
4250                    ob.add_scalarbar(title=title)
4251                    self.add(ob.scalarbar).render()
4252                elif isinstance(ob.scalarbar, vedo.Assembly):
4253                    self.remove(ob.scalarbar)
4254                    ob.add_scalarbar3d(title=ob._cmap_name)
4255                    self.add(ob.scalarbar)
4256
4257            vedo.printc(
4258                f"Name:'{ob.name}'," if ob.name else "",
4259                f"range:{utils.precision(ob.mapper.GetScalarRange(),3)},",
4260                f"colormap:'{ob._cmap_name}'", c="g", bold=False,
4261            )
4262
4263        elif key == "5":  # cycle pointdata array
4264            ob = self.clicked_object
4265            if not isinstance(ob, (vedo.Points, vedo.UnstructuredGrid)):
4266                return
4267
4268            arrnames = ob.pointdata.keys()
4269            arrnames = [a for a in arrnames if "normal" not in a.lower()]
4270            arrnames = [a for a in arrnames if "tcoord" not in a.lower()]
4271            arrnames = [a for a in arrnames if "textur" not in a.lower()]
4272            if len(arrnames) == 0:
4273                return
4274            ob.mapper.SetScalarVisibility(1)
4275
4276            if not ob._cmap_name:
4277                ob._cmap_name = "rainbow"
4278
4279            try:
4280                curr_name = ob.dataset.GetPointData().GetScalars().GetName()
4281                i = arrnames.index(curr_name)
4282                if "normals" in curr_name.lower():
4283                    return
4284                if iren.GetShiftKey():
4285                    i -= 1
4286                else:
4287                    i += 1
4288                if i >= len(arrnames):
4289                    i = 0
4290                if i < 0:
4291                    i = len(arrnames) - 1
4292            except (ValueError, AttributeError):
4293                i = 0
4294
4295            ob.cmap(ob._cmap_name, arrnames[i], on="points")
4296            if ob.scalarbar:
4297                if isinstance(ob.scalarbar, vtki.vtkActor2D):
4298                    self.remove(ob.scalarbar)
4299                    title = ob.scalarbar.GetTitle()
4300                    ob.scalarbar = None
4301                    ob.add_scalarbar(title=arrnames[i])
4302                    self.add(ob.scalarbar)
4303                elif isinstance(ob.scalarbar, vedo.Assembly):
4304                    self.remove(ob.scalarbar)
4305                    ob.scalarbar = None
4306                    ob.add_scalarbar3d(title=arrnames[i])
4307                    self.add(ob.scalarbar)
4308            else:
4309                vedo.printc(
4310                    f"Name:'{ob.name}'," if ob.name else "",
4311                    f"active pointdata array: '{arrnames[i]}'",
4312                    c="g", bold=False,
4313                )
4314
4315        elif key == "6":  # cycle celldata array
4316            ob = self.clicked_object
4317            if not isinstance(ob, (vedo.Points, vedo.UnstructuredGrid)):
4318                return
4319
4320            arrnames = ob.celldata.keys()
4321            arrnames = [a for a in arrnames if "normal" not in a.lower()]
4322            arrnames = [a for a in arrnames if "tcoord" not in a.lower()]
4323            arrnames = [a for a in arrnames if "textur" not in a.lower()]
4324            if len(arrnames) == 0:
4325                return
4326            ob.mapper.SetScalarVisibility(1)
4327
4328            if not ob._cmap_name:
4329                ob._cmap_name = "rainbow"
4330
4331            try:
4332                curr_name = ob.dataset.GetCellData().GetScalars().GetName()
4333                i = arrnames.index(curr_name)
4334                if "normals" in curr_name.lower():
4335                    return
4336                if iren.GetShiftKey():
4337                    i -= 1
4338                else:
4339                    i += 1
4340                if i >= len(arrnames):
4341                    i = 0
4342                if i < 0:
4343                    i = len(arrnames) - 1
4344            except (ValueError, AttributeError):
4345                i = 0
4346
4347            ob.cmap(ob._cmap_name, arrnames[i], on="cells")
4348            if ob.scalarbar:
4349                if isinstance(ob.scalarbar, vtki.vtkActor2D):
4350                    self.remove(ob.scalarbar)
4351                    title = ob.scalarbar.GetTitle()
4352                    ob.scalarbar = None
4353                    ob.add_scalarbar(title=arrnames[i])
4354                    self.add(ob.scalarbar)
4355                elif isinstance(ob.scalarbar, vedo.Assembly):
4356                    self.remove(ob.scalarbar)
4357                    ob.scalarbar = None
4358                    ob.add_scalarbar3d(title=arrnames[i])
4359                    self.add(ob.scalarbar)
4360            else:
4361                vedo.printc(
4362                    f"Name:'{ob.name}'," if ob.name else "",
4363                    f"active celldata array: '{arrnames[i]}'",
4364                    c="g", bold=False,
4365                )
4366
4367        elif key == "7":
4368            bgc = np.array(renderer.GetBackground()).sum() / 3
4369            if bgc <= 0:
4370                bgc = 0.223
4371            elif 0 < bgc < 1:
4372                bgc = 1
4373            else:
4374                bgc = 0
4375            renderer.SetBackground(bgc, bgc, bgc)
4376
4377        elif key == "8":
4378            bg2cols = [
4379                "lightyellow",
4380                "darkseagreen",
4381                "palegreen",
4382                "steelblue",
4383                "lightblue",
4384                "cadetblue",
4385                "lavender",
4386                "white",
4387                "blackboard",
4388                "black",
4389            ]
4390            bg2name = vedo.get_color_name(renderer.GetBackground2())
4391            if bg2name in bg2cols:
4392                idx = bg2cols.index(bg2name)
4393            else:
4394                idx = 4
4395            if idx is not None:
4396                bg2name_next = bg2cols[(idx + 1) % (len(bg2cols) - 1)]
4397            if not bg2name_next:
4398                renderer.GradientBackgroundOff()
4399            else:
4400                renderer.GradientBackgroundOn()
4401                renderer.SetBackground2(vedo.get_color(bg2name_next))
4402
4403        elif key in ["plus", "equal", "KP_Add", "minus", "KP_Subtract"]:  # cycle axes style
4404            i = self.renderers.index(renderer)
4405            try:
4406                self.axes_instances[i].EnabledOff()
4407                self.axes_instances[i].SetInteractor(None)
4408            except AttributeError:
4409                # print("Cannot remove widget", [self.axes_instances[i]])
4410                try:
4411                    self.remove(self.axes_instances[i])
4412                except:
4413                    print("Cannot remove axes", [self.axes_instances[i]])
4414                    return
4415            self.axes_instances[i] = None
4416
4417            if not self.axes:
4418                self.axes = 0
4419            if isinstance(self.axes, dict):
4420                self.axes = 1
4421
4422            if key in ["minus", "KP_Subtract"]:
4423                if not self.camera.GetParallelProjection() and self.axes == 0:
4424                    self.axes -= 1  # jump ruler doesnt make sense in perspective mode
4425                bns = self.renderer.ComputeVisiblePropBounds()
4426                addons.add_global_axes(axtype=(self.axes - 1) % 15, c=None, bounds=bns)
4427            else:
4428                if not self.camera.GetParallelProjection() and self.axes == 12:
4429                    self.axes += 1  # jump ruler doesnt make sense in perspective mode
4430                bns = self.renderer.ComputeVisiblePropBounds()
4431                addons.add_global_axes(axtype=(self.axes + 1) % 15, c=None, bounds=bns)
4432            self.render()
4433
4434        elif "KP_" in key or key in [
4435                "Insert","End","Down","Next","Left","Begin","Right","Home","Up","Prior"
4436            ]:
4437            asso = {  # change axes style
4438                "KP_Insert": 0, "KP_0": 0, "Insert": 0,
4439                "KP_End":    1, "KP_1": 1, "End":    1,
4440                "KP_Down":   2, "KP_2": 2, "Down":   2,
4441                "KP_Next":   3, "KP_3": 3, "Next":   3,
4442                "KP_Left":   4, "KP_4": 4, "Left":   4,
4443                "KP_Begin":  5, "KP_5": 5, "Begin":  5,
4444                "KP_Right":  6, "KP_6": 6, "Right":  6,
4445                "KP_Home":   7, "KP_7": 7, "Home":   7,
4446                "KP_Up":     8, "KP_8": 8, "Up":     8,
4447                "Prior":     9,  # on windows OS
4448            }
4449            clickedr = self.renderers.index(renderer)
4450            if key in asso:
4451                if self.axes_instances[clickedr]:
4452                    if hasattr(self.axes_instances[clickedr], "EnabledOff"):  # widget
4453                        self.axes_instances[clickedr].EnabledOff()
4454                    else:
4455                        try:
4456                            renderer.RemoveActor(self.axes_instances[clickedr])
4457                        except:
4458                            pass
4459                    self.axes_instances[clickedr] = None
4460                bounds = renderer.ComputeVisiblePropBounds()
4461                addons.add_global_axes(axtype=asso[key], c=None, bounds=bounds)
4462                self.interactor.Render()
4463
4464        if key == "O":
4465            renderer.RemoveLight(self._extralight)
4466            self._extralight = None
4467
4468        elif key == "