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