vedo.visual

Base classes to manage visualization and apperance of objects and their properties.

   1#!/usr/bin/env python3
   2# -*- coding: utf-8 -*-
   3import os
   4from typing import Union
   5from typing_extensions import Self
   6from weakref import ref as weak_ref_to
   7import numpy as np
   8
   9import vedo.vtkclasses as vtki
  10
  11import vedo
  12from vedo import colors
  13from vedo import utils
  14
  15
  16__docformat__ = "google"
  17
  18__doc__ = """
  19Base classes to manage visualization and apperance of objects and their properties.
  20"""
  21
  22__all__ = [
  23    "CommonVisual",
  24    "PointsVisual",
  25    "VolumeVisual",
  26    "MeshVisual",
  27    "ImageVisual",
  28    "Actor2D",
  29    "LightKit",
  30]
  31
  32
  33###################################################
  34class CommonVisual:
  35    """Class to manage the visual aspects common to all objects."""
  36
  37    def __init__(self):
  38        # print("init CommonVisual", type(self))
  39
  40        self.properties = None
  41
  42        self.scalarbar = None       
  43        self.pipeline = None
  44
  45        self.trail = None
  46        self.trail_points = []
  47        self.trail_segment_size = 0
  48        self.trail_offset = None
  49
  50        self.shadows = []
  51
  52        self.axes = None
  53        self.picked3d = None
  54
  55        self.rendered_at = set()
  56
  57        self._ligthingnr = 0  # index of the lighting mode changed from CLI
  58        self._cmap_name = ""  # remember the cmap name for self._keypress
  59        self._caption = None
  60
  61
  62    def print(self):
  63        """Print object info."""
  64        print(self.__str__())
  65        return self
  66
  67    @property
  68    def LUT(self) -> np.ndarray:
  69        """Return the lookup table of the object as a numpy object."""
  70        try:
  71            _lut = self.mapper.GetLookupTable()
  72            values = []
  73            for i in range(_lut.GetTable().GetNumberOfTuples()):
  74                # print("LUT i =", i, "value =", _lut.GetTableValue(i))
  75                values.append(_lut.GetTableValue(i))
  76            return np.array(values)
  77        except AttributeError:
  78            return np.array([], dtype=float)
  79        
  80    @LUT.setter
  81    def LUT(self, arr):
  82        """
  83        Set the lookup table of the object from a numpy or `vtkLookupTable` object.
  84        Consider using `cmap()` or `build_lut()` instead as it allows
  85        to set the range of the LUT and to use a string name for the color map.
  86        """
  87        if isinstance(arr, vtki.vtkLookupTable):
  88            newlut = arr
  89            self.mapper.SetScalarRange(newlut.GetRange())
  90        else:
  91            newlut = vtki.vtkLookupTable()
  92            newlut.SetNumberOfTableValues(len(arr))
  93            if len(arr[0]) == 3:
  94                arr = np.insert(arr, 3, 1, axis=1)
  95            for i, v in enumerate(arr):
  96                newlut.SetTableValue(i, v)
  97            newlut.SetRange(self.mapper.GetScalarRange())
  98            # print("newlut.GetRange() =", newlut.GetRange())
  99            # print("self.mapper.GetScalarRange() =", self.mapper.GetScalarRange())
 100            newlut.Build()
 101        self.mapper.SetLookupTable(newlut)
 102        self.mapper.ScalarVisibilityOn()
 103    
 104    def scalar_range(self, vmin=None, vmax=None):
 105        """Set the range of the scalar value for visualization."""
 106        if vmin is None and vmax is None:
 107            return np.array(self.mapper.GetScalarRange())
 108        if vmax is None:
 109            vmin, vmax = vmin # assume it is a list
 110        self.mapper.SetScalarRange(float(vmin), float(vmax))
 111        return self
 112
 113    def add_observer(self, event_name, func, priority=0) -> int:
 114        """Add a callback function that will be called when an event occurs."""
 115        event_name = utils.get_vtk_name_event(event_name)
 116        idd = self.actor.AddObserver(event_name, func, priority)
 117        return idd
 118    
 119    def invoke_event(self, event_name) -> Self:
 120        """Invoke an event."""
 121        event_name = utils.get_vtk_name_event(event_name)
 122        self.actor.InvokeEvent(event_name)
 123        return self
 124    
 125    # def abort_event(self, obs_id):
 126    #     """Abort an event."""
 127    #     cmd = self.actor.GetCommand(obs_id) # vtkCommand
 128    #     if cmd:
 129    #         cmd.AbortFlagOn()
 130    #     return self
 131
 132    def show(self, **options) -> Union["vedo.Plotter", None]:
 133        """
 134        Create on the fly an instance of class `Plotter` or use the last existing one to
 135        show one single object.
 136
 137        This method is meant as a shortcut. If more than one object needs to be visualised
 138        please use the syntax `show(mesh1, mesh2, volume, ..., options)`.
 139
 140        Returns the `Plotter` class instance.
 141        """
 142        return vedo.plotter.show(self, **options)
 143
 144    def thumbnail(self, zoom=1.25, size=(200, 200), bg="white", azimuth=0, elevation=0, axes=False) -> np.ndarray:
 145        """Build a thumbnail of the object and return it as an array."""
 146        # speed is about 20Hz for size=[200,200]
 147        ren = vtki.vtkRenderer()
 148
 149        actor = self.actor
 150        if isinstance(self, vedo.UnstructuredGrid):
 151            geo = vtki.new("GeometryFilter")
 152            geo.SetInputData(self.dataset)
 153            geo.Update()
 154            actor = vedo.Mesh(geo.GetOutput()).cmap("rainbow").actor
 155
 156        ren.AddActor(actor)
 157        if axes:
 158            axes = vedo.addons.Axes(self)
 159            ren.AddActor(axes.actor)
 160        ren.ResetCamera()
 161        cam = ren.GetActiveCamera()
 162        cam.Zoom(zoom)
 163        cam.Elevation(elevation)
 164        cam.Azimuth(azimuth)
 165
 166        ren_win = vtki.vtkRenderWindow()
 167        ren_win.SetOffScreenRendering(True)
 168        ren_win.SetSize(size)
 169        ren.SetBackground(colors.get_color(bg))
 170        ren_win.AddRenderer(ren)
 171        ren.ResetCameraClippingRange()
 172        ren_win.Render()
 173
 174        nx, ny = ren_win.GetSize()
 175        arr = vtki.vtkUnsignedCharArray()
 176        ren_win.GetRGBACharPixelData(0, 0, nx - 1, ny - 1, 0, arr)
 177        narr = utils.vtk2numpy(arr).T[:3].T.reshape([ny, nx, 3])
 178        narr = np.ascontiguousarray(np.flip(narr, axis=0))
 179
 180        ren.RemoveActor(actor)
 181        if axes:
 182            ren.RemoveActor(axes.actor)
 183        ren_win.Finalize()
 184        del ren_win
 185        return narr
 186
 187    def pickable(self, value=None) -> Self:
 188        """Set/get the pickability property of an object."""
 189        if value is None:
 190            return self.actor.GetPickable()
 191        self.actor.SetPickable(value)
 192        return self
 193
 194    def use_bounds(self, value=True) -> Self:
 195        """
 196        Instruct the current camera to either take into account or ignore
 197        the object bounds when resetting.
 198        """
 199        self.actor.SetUseBounds(value)
 200        return self
 201
 202    def draggable(self, value=None) -> Self:  # NOT FUNCTIONAL?
 203        """Set/get the draggability property of an object."""
 204        if value is None:
 205            return self.actor.GetDragable()
 206        self.actor.SetDragable(value)
 207        return self
 208
 209    def on(self) -> Self:
 210        """Switch on  object visibility. Object is not removed."""
 211        self.actor.VisibilityOn()
 212        try:
 213            self.scalarbar.actor.VisibilityOn()
 214        except AttributeError:
 215            pass
 216        try:
 217            self.trail.actor.VisibilityOn()
 218        except AttributeError:
 219            pass
 220        try:
 221            for sh in self.shadows:
 222                sh.actor.VisibilityOn()
 223        except AttributeError:
 224            pass
 225        return self
 226
 227    def off(self) -> Self:
 228        """Switch off object visibility. Object is not removed."""
 229        self.actor.VisibilityOff()
 230        try:
 231            self.scalarbar.actor.VisibilityOff()
 232        except AttributeError:
 233            pass
 234        try:
 235            self.trail.actor.VisibilityOff()
 236        except AttributeError:
 237            pass
 238        try:
 239            for sh in self.shadows:
 240                sh.actor.VisibilityOff()
 241        except AttributeError:
 242            pass
 243        return self
 244
 245    def toggle(self) -> Self:
 246        """Toggle object visibility on/off."""
 247        v = self.actor.GetVisibility()
 248        if v:
 249            self.off()
 250        else:
 251            self.on()
 252        return self
 253
 254    def add_scalarbar(
 255        self,
 256        title="",
 257        pos=(),
 258        size=(80, 400),
 259        font_size=14,
 260        title_yoffset=15,
 261        nlabels=None,
 262        c=None,
 263        horizontal=False,
 264        use_alpha=True,
 265        label_format=":6.3g",
 266    ) -> Self:
 267        """
 268        Add a 2D scalar bar for the specified object.
 269
 270        Arguments:
 271            title : (str)
 272                scalar bar title
 273            pos : (list)
 274                position coordinates of the bottom left corner.
 275                Can also be a pair of (x,y) values in the range [0,1]
 276                to indicate the position of the bottom left and top right corners.
 277            size : (float,float)
 278                size of the scalarbar in number of pixels (width, height)
 279            font_size : (float)
 280                size of font for title and numeric labels
 281            title_yoffset : (float)
 282                vertical space offset between title and color scalarbar
 283            nlabels : (int)
 284                number of numeric labels
 285            c : (list)
 286                color of the scalar bar text
 287            horizontal : (bool)
 288                lay the scalarbar horizontally
 289            use_alpha : (bool)
 290                render transparency in the color bar itself
 291            label_format : (str)
 292                c-style format string for numeric labels
 293
 294        Examples:
 295            - [mesh_coloring.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mesh_coloring.py)
 296            - [scalarbars.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/scalarbars.py)
 297        """
 298        plt = vedo.plotter_instance
 299
 300        if plt and plt.renderer:
 301            c = (0.9, 0.9, 0.9)
 302            if np.sum(plt.renderer.GetBackground()) > 1.5:
 303                c = (0.1, 0.1, 0.1)
 304            if isinstance(self.scalarbar, vtki.vtkActor):
 305                plt.renderer.RemoveActor(self.scalarbar)
 306            elif isinstance(self.scalarbar, vedo.Assembly):
 307                for a in self.scalarbar.unpack():
 308                    plt.renderer.RemoveActor(a)
 309        if c is None:
 310            c = "gray"
 311
 312        sb = vedo.addons.ScalarBar(
 313            self,
 314            title,
 315            pos,
 316            size,
 317            font_size,
 318            title_yoffset,
 319            nlabels,
 320            c,
 321            horizontal,
 322            use_alpha,
 323            label_format,
 324        )
 325        self.scalarbar = sb
 326        return self
 327
 328    def add_scalarbar3d(
 329        self,
 330        title="",
 331        pos=None,
 332        size=(0, 0),
 333        title_font="",
 334        title_xoffset=-1.2,
 335        title_yoffset=0.0,
 336        title_size=1.5,
 337        title_rotation=0.0,
 338        nlabels=9,
 339        label_font="",
 340        label_size=1,
 341        label_offset=0.375,
 342        label_rotation=0,
 343        label_format="",
 344        italic=0,
 345        c=None,
 346        draw_box=True,
 347        above_text=None,
 348        below_text=None,
 349        nan_text="NaN",
 350        categories=None,
 351    ) -> Self:
 352        """
 353        Associate a 3D scalar bar to the object and add it to the scene.
 354        The new scalarbar object (Assembly) will be accessible as obj.scalarbar
 355
 356        Arguments:
 357            size : (list)
 358                (thickness, length) of scalarbar
 359            title : (str)
 360                scalar bar title
 361            title_xoffset : (float)
 362                horizontal space btw title and color scalarbar
 363            title_yoffset : (float)
 364                vertical space offset
 365            title_size : (float)
 366                size of title wrt numeric labels
 367            title_rotation : (float)
 368                title rotation in degrees
 369            nlabels : (int)
 370                number of numeric labels
 371            label_font : (str)
 372                font type for labels
 373            label_size : (float)
 374                label scale factor
 375            label_offset : (float)
 376                space btw numeric labels and scale
 377            label_rotation : (float)
 378                label rotation in degrees
 379            label_format : (str)
 380                label format for floats and integers (e.g. `':.2f'`)
 381            draw_box : (bool)
 382                draw a box around the colorbar
 383            categories : (list)
 384                make a categorical scalarbar,
 385                the input list will have the format `[value, color, alpha, textlabel]`
 386
 387        Examples:
 388            - [scalarbars.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/scalarbars.py)
 389        """
 390        plt = vedo.plotter_instance
 391        if plt and c is None:  # automatic black or white
 392            c = (0.9, 0.9, 0.9)
 393            if np.sum(vedo.get_color(plt.backgrcol)) > 1.5:
 394                c = (0.1, 0.1, 0.1)
 395        if c is None:
 396            c = (0, 0, 0)
 397        c = vedo.get_color(c)
 398
 399        self.scalarbar = vedo.addons.ScalarBar3D(
 400            self,
 401            title,
 402            pos,
 403            size,
 404            title_font,
 405            title_xoffset,
 406            title_yoffset,
 407            title_size,
 408            title_rotation,
 409            nlabels,
 410            label_font,
 411            label_size,
 412            label_offset,
 413            label_rotation,
 414            label_format,
 415            italic,
 416            c,
 417            draw_box,
 418            above_text,
 419            below_text,
 420            nan_text,
 421            categories,
 422        )
 423        return self
 424
 425    def color(self, col, alpha=None, vmin=None, vmax=None):
 426        """
 427        Assign a color or a set of colors along the range of the scalar value.
 428        A single constant color can also be assigned.
 429        Any matplotlib color map name is also accepted, e.g. `volume.color('jet')`.
 430
 431        E.g.: say that your cells scalar runs from -3 to 6,
 432        and you want -3 to show red and 1.5 violet and 6 green, then just set:
 433
 434        `volume.color(['red', 'violet', 'green'])`
 435
 436        You can also assign a specific color to a aspecific value with eg.:
 437
 438        `volume.color([(0,'red'), (0.5,'violet'), (1,'green')])`
 439
 440        Arguments:
 441            alpha : (list)
 442                use a list to specify transparencies along the scalar range
 443            vmin : (float)
 444                force the min of the scalar range to be this value
 445            vmax : (float)
 446                force the max of the scalar range to be this value
 447        """
 448        # supersedes method in Points, Mesh
 449
 450        if col is None:
 451            return self
 452
 453        if vmin is None:
 454            vmin, _ = self.dataset.GetScalarRange()
 455        if vmax is None:
 456            _, vmax = self.dataset.GetScalarRange()
 457        ctf = self.properties.GetRGBTransferFunction()
 458        ctf.RemoveAllPoints()
 459
 460        if utils.is_sequence(col):
 461            if utils.is_sequence(col[0]) and len(col[0]) == 2:
 462                # user passing [(value1, color1), ...]
 463                for x, ci in col:
 464                    r, g, b = colors.get_color(ci)
 465                    ctf.AddRGBPoint(x, r, g, b)
 466                    # colors.printc('color at', round(x, 1),
 467                    #               'set to', colors.get_color_name((r, g, b)), bold=0)
 468            else:
 469                # user passing [color1, color2, ..]
 470                for i, ci in enumerate(col):
 471                    r, g, b = colors.get_color(ci)
 472                    x = vmin + (vmax - vmin) * i / (len(col) - 1)
 473                    ctf.AddRGBPoint(x, r, g, b)
 474        elif isinstance(col, str):
 475            if col in colors.colors.keys() or col in colors.color_nicks.keys():
 476                r, g, b = colors.get_color(col)
 477                ctf.AddRGBPoint(vmin, r, g, b)  # constant color
 478                ctf.AddRGBPoint(vmax, r, g, b)
 479            else:  # assume it's a colormap
 480                for x in np.linspace(vmin, vmax, num=64, endpoint=True):
 481                    r, g, b = colors.color_map(x, name=col, vmin=vmin, vmax=vmax)
 482                    ctf.AddRGBPoint(x, r, g, b)
 483        elif isinstance(col, int):
 484            r, g, b = colors.get_color(col)
 485            ctf.AddRGBPoint(vmin, r, g, b)  # constant color
 486            ctf.AddRGBPoint(vmax, r, g, b)
 487        else:
 488            vedo.logger.warning(f"in color() unknown input type {type(col)}")
 489
 490        if alpha is not None:
 491            self.alpha(alpha, vmin=vmin, vmax=vmax)
 492        return self
 493
 494    def alpha(self, alpha, vmin=None, vmax=None) -> Self:
 495        """
 496        Assign a set of tranparencies along the range of the scalar value.
 497        A single constant value can also be assigned.
 498
 499        E.g.: say `alpha=(0.0, 0.3, 0.9, 1)` and the scalar range goes from -10 to 150.
 500        Then all cells with a value close to -10 will be completely transparent, cells at 1/4
 501        of the range will get an alpha equal to 0.3 and voxels with value close to 150
 502        will be completely opaque.
 503
 504        As a second option one can set explicit (x, alpha_x) pairs to define the transfer function.
 505
 506        E.g.: say `alpha=[(-5, 0), (35, 0.4) (123,0.9)]` and the scalar range goes from -10 to 150.
 507        Then all cells below -5 will be completely transparent, cells with a scalar value of 35
 508        will get an opacity of 40% and above 123 alpha is set to 90%.
 509        """
 510        if vmin is None:
 511            vmin, _ = self.dataset.GetScalarRange()
 512        if vmax is None:
 513            _, vmax = self.dataset.GetScalarRange()
 514        otf = self.properties.GetScalarOpacity()
 515        otf.RemoveAllPoints()
 516
 517        if utils.is_sequence(alpha):
 518            alpha = np.array(alpha)
 519            if len(alpha.shape) == 1:  # user passing a flat list e.g. (0.0, 0.3, 0.9, 1)
 520                for i, al in enumerate(alpha):
 521                    xalpha = vmin + (vmax - vmin) * i / (len(alpha) - 1)
 522                    # Create transfer mapping scalar value to opacity
 523                    otf.AddPoint(xalpha, al)
 524                    # print("alpha at", round(xalpha, 1), "\tset to", al)
 525            elif len(alpha.shape) == 2:  # user passing [(x0,alpha0), ...]
 526                otf.AddPoint(vmin, alpha[0][1])
 527                for xalpha, al in alpha:
 528                    # Create transfer mapping scalar value to opacity
 529                    otf.AddPoint(xalpha, al)
 530                otf.AddPoint(vmax, alpha[-1][1])
 531
 532        else:
 533
 534            otf.AddPoint(vmin, alpha)  # constant alpha
 535            otf.AddPoint(vmax, alpha)
 536
 537        return self
 538
 539
 540########################################################################################
 541class Actor2D(vtki.vtkActor2D):
 542    """Wrapping of `vtkActor2D`."""
 543
 544    def __init__(self):
 545        """Manage 2D objects."""
 546        super().__init__()
 547
 548        self.dataset = None
 549        self.properties = self.GetProperty()
 550        self.name = ""
 551        self.filename = ""
 552        self.file_size = 0
 553        self.pipeline = None
 554        self.shape = [] # for images
 555
 556    @property
 557    def mapper(self):
 558        """Get the internal vtkMapper."""
 559        return self.GetMapper()
 560    
 561    @mapper.setter
 562    def mapper(self, amapper):
 563        """Set the internal vtkMapper."""
 564        self.SetMapper(amapper)
 565
 566    def layer(self, value=None):
 567        """Set/Get the layer number in the overlay planes into which to render."""
 568        if value is None:
 569            return self.GetLayerNumber()
 570        self.SetLayerNumber(value)
 571        return self
 572
 573    def pos(self, px=None, py=None) -> Union[np.ndarray, Self]:
 574        """Set/Get the screen-coordinate position."""
 575        if isinstance(px, str):
 576            vedo.logger.error("Use string descriptors only inside the constructor")
 577            return self
 578        if px is None:
 579            return np.array(self.GetPosition(), dtype=int)
 580        if py is not None:
 581            p = [px, py]
 582        else:
 583            p = px
 584        assert len(p) == 2, "Error: len(pos) must be 2 for Actor2D"
 585        self.SetPosition(p)
 586        return self
 587
 588    def coordinate_system(self, value=None) -> Self:
 589        """
 590        Set/get the coordinate system which this coordinate is defined in.
 591
 592        The options are:
 593            0. Display
 594            1. Normalized Display
 595            2. Viewport
 596            3. Normalized Viewport
 597            4. View
 598            5. Pose
 599            6. World
 600        """
 601        coor = self.GetPositionCoordinate()
 602        if value is None:
 603            return coor.GetCoordinateSystem()
 604        coor.SetCoordinateSystem(value)
 605        return self
 606
 607    def on(self) -> Self:
 608        """Set object visibility."""
 609        self.VisibilityOn()
 610        return self
 611
 612    def off(self) -> Self:
 613        """Set object visibility."""
 614        self.VisibilityOn()
 615        return self
 616
 617    def toggle(self) -> Self:
 618        """Toggle object visibility."""
 619        self.SetVisibility(not self.GetVisibility())
 620        return self
 621
 622    def pickable(self, value=True) -> Self:
 623        """Set object pickability."""
 624        self.SetPickable(value)
 625        return self
 626
 627    def color(self, value=None) -> Union[np.ndarray, Self]:
 628        """Set/Get the object color."""
 629        if value is None:
 630            return self.properties.GetColor()
 631        self.properties.SetColor(colors.get_color(value))
 632        return self
 633
 634    def c(self, value=None) -> Union[np.ndarray, Self]:
 635        """Set/Get the object color."""
 636        return self.color(value)
 637
 638    def alpha(self, value=None) -> Union[float, Self]:
 639        """Set/Get the object opacity."""
 640        if value is None:
 641            return self.properties.GetOpacity()
 642        self.properties.SetOpacity(value)
 643        return self
 644
 645    def ps(self, point_size=None) -> Union[int, Self]:
 646        if point_size is None:
 647            return self.properties.GetPointSize()
 648        self.properties.SetPointSize(point_size)
 649        return self
 650
 651    def lw(self, line_width=None) -> Union[int, Self]:
 652        if line_width is None:
 653            return self.properties.GetLineWidth()
 654        self.properties.SetLineWidth(line_width)
 655        return self
 656
 657    def ontop(self, value=True) -> Self:
 658        """Keep the object always on top of everything else."""
 659        if value:
 660            self.properties.SetDisplayLocationToForeground()
 661        else:
 662            self.properties.SetDisplayLocationToBackground()
 663        return self
 664
 665    def add_observer(self, event_name, func, priority=0) -> int:
 666        """Add a callback function that will be called when an event occurs."""
 667        event_name = utils.get_vtk_name_event(event_name)
 668        idd = self.AddObserver(event_name, func, priority)
 669        return idd
 670
 671########################################################################################
 672class Actor3DHelper:
 673
 674    def apply_transform(self, LT) -> Self:
 675        """Apply a linear transformation to the actor."""
 676        self.transform.concatenate(LT)
 677        self.actor.SetPosition(self.transform.T.GetPosition())
 678        self.actor.SetOrientation(self.transform.T.GetOrientation())
 679        self.actor.SetScale(self.transform.T.GetScale())
 680        return self
 681
 682    def pos(self, x=None, y=None, z=None) -> Union[np.ndarray, Self]:
 683        """Set/Get object position."""
 684        if x is None:  # get functionality
 685            return self.transform.position
 686
 687        if z is None and y is None:  # assume x is of the form (x,y,z)
 688            if len(x) == 3:
 689                x, y, z = x
 690            else:
 691                x, y = x
 692                z = 0
 693        elif z is None:  # assume x,y is of the form x, y
 694            z = 0
 695
 696        q = self.transform.position
 697        LT = vedo.LinearTransform().translate([x,y,z]-q)
 698        return self.apply_transform(LT)
 699
 700    def shift(self, dx, dy=0, dz=0) -> Self:
 701        """Add a vector to the current object position."""
 702        if vedo.utils.is_sequence(dx):
 703            vedo.utils.make3d(dx)
 704            dx, dy, dz = dx
 705        LT = vedo.LinearTransform().translate([dx, dy, dz])
 706        return self.apply_transform(LT)
 707
 708    def origin(self, point=None) -> Union[np.ndarray, Self]:
 709        """
 710        Set/get origin of object.
 711        Useful for defining pivoting point when rotating and/or scaling.
 712        """
 713        if point is None:
 714            return np.array(self.actor.GetOrigin())
 715        self.actor.SetOrigin(point)
 716        return self
 717
 718    def scale(self, s, origin=True) -> Self:
 719        """Multiply object size by `s` factor."""
 720        LT = vedo.LinearTransform().scale(s, origin=origin)
 721        return self.apply_transform(LT)
 722
 723    def x(self, val=None) -> Union[float, Self]:
 724        """Set/Get object position along x axis."""
 725        p = self.transform.position
 726        if val is None:
 727            return p[0]
 728        self.pos(val, p[1], p[2])
 729        return self
 730
 731    def y(self, val=None) -> Union[float, Self]:
 732        """Set/Get object position along y axis."""
 733        p = self.transform.position
 734        if val is None:
 735            return p[1]
 736        self.pos(p[0], val, p[2])
 737        return self
 738
 739    def z(self, val=None) -> Union[float, Self]:
 740        """Set/Get object position along z axis."""
 741        p = self.transform.position
 742        if val is None:
 743            return p[2]
 744        self.pos(p[0], p[1], val)
 745        return self
 746
 747    def rotate_x(self, angle) -> Self:
 748        """Rotate object around x axis."""
 749        LT = vedo.LinearTransform().rotate_x(angle)
 750        return self.apply_transform(LT)
 751
 752    def rotate_y(self, angle) -> Self:
 753        """Rotate object around y axis."""
 754        LT = vedo.LinearTransform().rotate_y(angle)
 755        return self.apply_transform(LT)
 756
 757    def rotate_z(self, angle) -> Self:
 758        """Rotate object around z axis."""
 759        LT = vedo.LinearTransform().rotate_z(angle)
 760        return self.apply_transform(LT)
 761
 762    def reorient(self, old_axis, new_axis, rotation=0, rad=False) -> Self:
 763        """Rotate object to a new orientation."""
 764        if rad:
 765            rotation *= 180 / np.pi
 766        axis = old_axis / np.linalg.norm(old_axis)
 767        direction = new_axis / np.linalg.norm(new_axis)
 768        angle = np.arccos(np.dot(axis, direction)) * 180 / np.pi
 769        self.actor.RotateZ(rotation)
 770        a,b,c = np.cross(axis, direction)
 771        self.actor.RotateWXYZ(angle, c,b,a)
 772        return self
 773
 774    def bounds(self) -> np.ndarray:
 775        """
 776        Get the object bounds.
 777        Returns a list in format `[xmin,xmax, ymin,ymax, zmin,zmax]`.
 778        """
 779        return np.array(self.actor.GetBounds())
 780
 781    def xbounds(self, i=None) -> Union[float, tuple]:
 782        """Get the bounds `[xmin,xmax]`. Can specify upper or lower with i (0,1)."""
 783        b = self.bounds()
 784        if i is not None:
 785            return b[i]
 786        return (b[0], b[1])
 787
 788    def ybounds(self, i=None) -> Union[float, tuple]:
 789        """Get the bounds `[ymin,ymax]`. Can specify upper or lower with i (0,1)."""
 790        b = self.bounds()
 791        if i == 0:
 792            return b[2]
 793        if i == 1:
 794            return b[3]
 795        return (b[2], b[3])
 796
 797    def zbounds(self, i=None) -> Union[float, tuple]:
 798        """Get the bounds `[zmin,zmax]`. Can specify upper or lower with i (0,1)."""
 799        b = self.bounds()
 800        if i == 0:
 801            return b[4]
 802        if i == 1:
 803            return b[5]
 804        return (b[4], b[5])
 805    
 806    def diagonal_size(self) -> float:
 807        """Get the diagonal size of the bounding box."""
 808        b = self.bounds()
 809        return np.sqrt((b[1]-b[0])**2 + (b[3]-b[2])**2 + (b[5]-b[4])**2)
 810
 811###################################################
 812class PointsVisual(CommonVisual):
 813    """Class to manage the visual aspects of a ``Points`` object."""
 814
 815    def __init__(self):
 816        # print("init PointsVisual")
 817        super().__init__()
 818        self.properties_backface = None
 819        self._cmap_name = None
 820        self.trail = None
 821        self.trail_offset = 0
 822        self.trail_points = []
 823        self._caption = None
 824        
 825
 826    def clone2d(self, size=None, offset=(), scale=None):
 827        """
 828        Turn a 3D `Points` or `Mesh` into a flat 2D actor.
 829        Returns a `Actor2D`.
 830
 831        Arguments:
 832            size : (float)
 833                size as scaling factor for the 2D actor
 834            offset : (list)
 835                2D (x, y) position of the actor in the range [-1, 1]
 836            scale : (float)
 837                Deprecated. Use `size` instead.
 838
 839        Examples:
 840            - [clone2d.py](https://github.com/marcomusy/vedo/tree/master/examples/other/clone2d.py)
 841
 842                ![](https://vedo.embl.es/images/other/clone2d.png)
 843        """
 844        # assembly.Assembly.clone2d() superseeds this method
 845        if scale is not None:
 846            vedo.logger.warning("clone2d(): use keyword size not scale")
 847            size = scale
 848
 849        if size is None:
 850            # work out a reasonable scale
 851            msiz = self.diagonal_size()
 852            if vedo.plotter_instance and vedo.plotter_instance.window:
 853                sz = vedo.plotter_instance.window.GetSize()
 854                dsiz = utils.mag(sz)
 855                size = dsiz / msiz / 10
 856            else:
 857                size = 350 / msiz
 858
 859        tp = vtki.new("TransformPolyDataFilter")
 860        transform = vtki.vtkTransform()
 861        transform.Scale(size, size, size)
 862        if len(offset) == 0:
 863            offset = self.pos()
 864        transform.Translate(-utils.make3d(offset))
 865        tp.SetTransform(transform)
 866        tp.SetInputData(self.dataset)
 867        tp.Update()
 868        poly = tp.GetOutput()
 869
 870        cm = self.mapper.GetColorMode()
 871        lut = self.mapper.GetLookupTable()
 872        sv = self.mapper.GetScalarVisibility()
 873        use_lut = self.mapper.GetUseLookupTableScalarRange()
 874        vrange = self.mapper.GetScalarRange()
 875        sm = self.mapper.GetScalarMode()
 876
 877        act2d = Actor2D()
 878        act2d.dataset = poly
 879        mapper2d = vtki.new("PolyDataMapper2D")
 880        mapper2d.SetInputData(poly)
 881        mapper2d.SetColorMode(cm)
 882        mapper2d.SetLookupTable(lut)
 883        mapper2d.SetScalarVisibility(sv)
 884        mapper2d.SetUseLookupTableScalarRange(use_lut)
 885        mapper2d.SetScalarRange(vrange)
 886        mapper2d.SetScalarMode(sm)
 887        act2d.mapper = mapper2d
 888
 889        act2d.GetPositionCoordinate().SetCoordinateSystem(4)
 890        act2d.properties.SetColor(self.color())
 891        act2d.properties.SetOpacity(self.alpha())
 892        act2d.properties.SetLineWidth(self.properties.GetLineWidth())
 893        act2d.properties.SetPointSize(self.properties.GetPointSize())
 894        act2d.properties.SetDisplayLocation(0) # 0 = back, 1 = front
 895        act2d.PickableOff()
 896        return act2d
 897
 898    ##################################################
 899    def copy_properties_from(self, source, deep=True, actor_related=True) -> Self:
 900        """
 901        Copy properties from another ``Points`` object.
 902        """
 903        pr = vtki.vtkProperty()
 904        try:
 905            sp = source.properties
 906            mp = source.mapper
 907            sa = source.actor
 908        except AttributeError:
 909            sp = source.GetProperty()
 910            mp = source.GetMapper()
 911            sa = source
 912            
 913        if deep:
 914            pr.DeepCopy(sp)
 915        else:
 916            pr.ShallowCopy(sp)
 917        self.actor.SetProperty(pr)
 918        self.properties = pr
 919
 920        if self.actor.GetBackfaceProperty():
 921            bfpr = vtki.vtkProperty()
 922            bfpr.DeepCopy(sa.GetBackfaceProperty())
 923            self.actor.SetBackfaceProperty(bfpr)
 924            self.properties_backface = bfpr
 925
 926        if not actor_related:
 927            return self
 928
 929        # mapper related:
 930        self.mapper.SetScalarVisibility(mp.GetScalarVisibility())
 931        self.mapper.SetScalarMode(mp.GetScalarMode())
 932        self.mapper.SetScalarRange(mp.GetScalarRange())
 933        self.mapper.SetLookupTable(mp.GetLookupTable())
 934        self.mapper.SetColorMode(mp.GetColorMode())
 935        self.mapper.SetInterpolateScalarsBeforeMapping(
 936            mp.GetInterpolateScalarsBeforeMapping()
 937        )
 938        self.mapper.SetUseLookupTableScalarRange(
 939            mp.GetUseLookupTableScalarRange()
 940        )
 941
 942        self.actor.SetPickable(sa.GetPickable())
 943        self.actor.SetDragable(sa.GetDragable())
 944        self.actor.SetTexture(sa.GetTexture())
 945        self.actor.SetVisibility(sa.GetVisibility())
 946        return self
 947
 948    def color(self, c=False, alpha=None) -> Union[np.ndarray, Self]:
 949        """
 950        Set/get mesh's color.
 951        If None is passed as input, will use colors from active scalars.
 952        Same as `mesh.c()`.
 953        """
 954        if c is False:
 955            return np.array(self.properties.GetColor())
 956        if c is None:
 957            self.mapper.ScalarVisibilityOn()
 958            return self
 959        self.mapper.ScalarVisibilityOff()
 960        cc = colors.get_color(c)
 961        self.properties.SetColor(cc)
 962        if self.trail:
 963            self.trail.properties.SetColor(cc)
 964        if alpha is not None:
 965            self.alpha(alpha)
 966        return self
 967
 968    def c(self, color=False, alpha=None) -> Union[np.ndarray, Self]:
 969        """
 970        Shortcut for `color()`.
 971        If None is passed as input, will use colors from current active scalars.
 972        """
 973        return self.color(color, alpha)
 974
 975    def alpha(self, opacity=None) -> Union[float, Self]:
 976        """Set/get mesh's transparency. Same as `mesh.opacity()`."""
 977        if opacity is None:
 978            return self.properties.GetOpacity()
 979
 980        self.properties.SetOpacity(opacity)
 981        bfp = self.actor.GetBackfaceProperty()
 982        if bfp:
 983            if opacity < 1:
 984                self.properties_backface = bfp
 985                self.actor.SetBackfaceProperty(None)
 986            else:
 987                self.actor.SetBackfaceProperty(self.properties_backface)
 988        return self
 989
 990    def lut_color_at(self, value) -> np.ndarray:
 991        """
 992        Return the color and alpha in the lookup table at given value.
 993        """
 994        lut = self.mapper.GetLookupTable()
 995        if not lut:
 996            return None
 997        rgb = [0,0,0]
 998        lut.GetColor(value, rgb)
 999        alpha = lut.GetOpacity(value)
1000        return np.array(rgb + [alpha])
1001
1002    def opacity(self, alpha=None) -> Union[float, Self]:
1003        """Set/get mesh's transparency. Same as `mesh.alpha()`."""
1004        return self.alpha(alpha)
1005
1006    def force_opaque(self, value=True) -> Self:
1007        """ Force the Mesh, Line or point cloud to be treated as opaque"""
1008        ## force the opaque pass, fixes picking in vtk9
1009        # but causes other bad troubles with lines..
1010        self.actor.SetForceOpaque(value)
1011        return self
1012
1013    def force_translucent(self, value=True) -> Self:
1014        """ Force the Mesh, Line or point cloud to be treated as translucent"""
1015        self.actor.SetForceTranslucent(value)
1016        return self
1017
1018    def point_size(self, value=None) -> Union[int, Self]:
1019        """Set/get mesh's point size of vertices. Same as `mesh.ps()`"""
1020        if value is None:
1021            return self.properties.GetPointSize()
1022            # self.properties.SetRepresentationToSurface()
1023        else:
1024            self.properties.SetRepresentationToPoints()
1025            self.properties.SetPointSize(value)
1026        return self
1027
1028    def ps(self, pointsize=None) -> Union[int, Self]:
1029        """Set/get mesh's point size of vertices. Same as `mesh.point_size()`"""
1030        return self.point_size(pointsize)
1031
1032    def render_points_as_spheres(self, value=True) -> Self:
1033        """Make points look spheric or else make them look as squares."""
1034        self.properties.SetRenderPointsAsSpheres(value)
1035        return self
1036
1037    def lighting(
1038        self,
1039        style="",
1040        ambient=None,
1041        diffuse=None,
1042        specular=None,
1043        specular_power=None,
1044        specular_color=None,
1045        metallicity=None,
1046        roughness=None,
1047    ) -> Self:
1048        """
1049        Set the ambient, diffuse, specular and specular_power lighting constants.
1050
1051        Arguments:
1052            style : (str)
1053                preset style, options are `[metallic, plastic, shiny, glossy, ambient, off]`
1054            ambient : (float)
1055                ambient fraction of emission [0-1]
1056            diffuse : (float)
1057                emission of diffused light in fraction [0-1]
1058            specular : (float)
1059                fraction of reflected light [0-1]
1060            specular_power : (float)
1061                precision of reflection [1-100]
1062            specular_color : (color)
1063                color that is being reflected by the surface
1064
1065        <img src="https://upload.wikimedia.org/wikipedia/commons/6/6b/Phong_components_version_4.png" alt="", width=700px>
1066
1067        Examples:
1068            - [specular.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/specular.py)
1069        """
1070        pr = self.properties
1071
1072        if style:
1073
1074            if style != "off":
1075                pr.LightingOn()
1076
1077            if style == "off":
1078                pr.SetInterpolationToFlat()
1079                pr.LightingOff()
1080                return self  ##############
1081
1082            if hasattr(pr, "GetColor"):  # could be Volume
1083                c = pr.GetColor()
1084            else:
1085                c = (1, 1, 0.99)
1086            mpr = self.mapper
1087            if hasattr(mpr, 'GetScalarVisibility') and mpr.GetScalarVisibility():
1088                c = (1,1,0.99)
1089            if   style=='metallic': pars = [0.1, 0.3, 1.0, 10, c]
1090            elif style=='plastic' : pars = [0.3, 0.4, 0.3,  5, c]
1091            elif style=='shiny'   : pars = [0.2, 0.6, 0.8, 50, c]
1092            elif style=='glossy'  : pars = [0.1, 0.7, 0.9, 90, (1,1,0.99)]
1093            elif style=='ambient' : pars = [0.8, 0.1, 0.0,  1, (1,1,1)]
1094            elif style=='default' : pars = [0.1, 1.0, 0.05, 5, c]
1095            else:
1096                vedo.logger.error("in lighting(): Available styles are")
1097                vedo.logger.error("[default, metallic, plastic, shiny, glossy, ambient, off]")
1098                raise RuntimeError()
1099            pr.SetAmbient(pars[0])
1100            pr.SetDiffuse(pars[1])
1101            pr.SetSpecular(pars[2])
1102            pr.SetSpecularPower(pars[3])
1103            if hasattr(pr, "GetColor"):
1104                pr.SetSpecularColor(pars[4])
1105
1106        if ambient is not None: pr.SetAmbient(ambient)
1107        if diffuse is not None: pr.SetDiffuse(diffuse)
1108        if specular is not None: pr.SetSpecular(specular)
1109        if specular_power is not None: pr.SetSpecularPower(specular_power)
1110        if specular_color is not None: pr.SetSpecularColor(colors.get_color(specular_color))
1111        if metallicity is not None:
1112            pr.SetInterpolationToPBR()
1113            pr.SetMetallic(metallicity)
1114        if roughness is not None:
1115            pr.SetInterpolationToPBR()
1116            pr.SetRoughness(roughness)
1117
1118        return self
1119
1120    def point_blurring(self, r=1, alpha=1.0, emissive=False) -> Self:
1121        """Set point blurring.
1122        Apply a gaussian convolution filter to the points.
1123        In this case the radius `r` is in absolute units of the mesh coordinates.
1124        With emissive set, the halo of point becomes light-emissive.
1125        """
1126        self.properties.SetRepresentationToPoints()
1127        if emissive:
1128            self.mapper.SetEmissive(bool(emissive))
1129        self.mapper.SetScaleFactor(r * 1.4142)
1130
1131        # https://kitware.github.io/vtk-examples/site/Python/Meshes/PointInterpolator/
1132        if alpha < 1:
1133            self.mapper.SetSplatShaderCode(
1134                "//VTK::Color::Impl\n"
1135                "float dist = dot(offsetVCVSOutput.xy,offsetVCVSOutput.xy);\n"
1136                "if (dist > 1.0) {\n"
1137                "   discard;\n"
1138                "} else {\n"
1139                f"  float scale = ({alpha} - dist);\n"
1140                "   ambientColor *= scale;\n"
1141                "   diffuseColor *= scale;\n"
1142                "}\n"
1143            )
1144            alpha = 1
1145
1146        self.mapper.Modified()
1147        self.actor.Modified()
1148        self.properties.SetOpacity(alpha)
1149        self.actor.SetMapper(self.mapper)
1150        return self
1151
1152    @property
1153    def cellcolors(self):
1154        """
1155        Colorize each cell (face) of a mesh by passing
1156        a 1-to-1 list of colors in format [R,G,B] or [R,G,B,A].
1157        Colors levels and opacities must be in the range [0,255].
1158
1159        A single constant color can also be passed as string or RGBA.
1160
1161        A cell array named "CellsRGBA" is automatically created.
1162
1163        Examples:
1164            - [color_mesh_cells1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/color_mesh_cells1.py)
1165            - [color_mesh_cells2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/color_mesh_cells2.py)
1166
1167            ![](https://vedo.embl.es/images/basic/colorMeshCells.png)
1168        """
1169        if "CellsRGBA" not in self.celldata.keys():
1170            lut = self.mapper.GetLookupTable()
1171            vscalars = self.dataset.GetCellData().GetScalars()
1172            if vscalars is None or lut is None:
1173                arr = np.zeros([self.ncells, 4], dtype=np.uint8)
1174                col = np.array(self.properties.GetColor())
1175                col = np.round(col * 255).astype(np.uint8)
1176                alf = self.properties.GetOpacity()
1177                alf = np.round(alf * 255).astype(np.uint8)
1178                arr[:, (0, 1, 2)] = col
1179                arr[:, 3] = alf
1180            else:
1181                cols = lut.MapScalars(vscalars, 0, 0)
1182                arr = utils.vtk2numpy(cols)
1183            self.celldata["CellsRGBA"] = arr
1184        self.celldata.select("CellsRGBA")
1185        return self.celldata["CellsRGBA"]
1186
1187    @cellcolors.setter
1188    def cellcolors(self, value):
1189        if isinstance(value, str):
1190            c = colors.get_color(value)
1191            value = np.array([*c, 1]) * 255
1192            value = np.round(value)
1193
1194        value = np.asarray(value)
1195        n = self.ncells
1196
1197        if value.ndim == 1:
1198            value = np.repeat([value], n, axis=0)
1199
1200        if value.shape[1] == 3:
1201            z = np.zeros((n, 1), dtype=np.uint8)
1202            value = np.append(value, z + 255, axis=1)
1203
1204        assert n == value.shape[0]
1205
1206        self.celldata["CellsRGBA"] = value.astype(np.uint8)
1207        # self.mapper.SetColorModeToDirectScalars() # done in select()
1208        self.celldata.select("CellsRGBA")
1209
1210    @property
1211    def pointcolors(self):
1212        """
1213        Colorize each point (or vertex of a mesh) by passing
1214        a 1-to-1 list of colors in format [R,G,B] or [R,G,B,A].
1215        Colors levels and opacities must be in the range [0,255].
1216
1217        A single constant color can also be passed as string or RGBA.
1218
1219        A point array named "PointsRGBA" is automatically created.
1220        """
1221        if "PointsRGBA" not in self.pointdata.keys():
1222            lut = self.mapper.GetLookupTable()
1223            vscalars = self.dataset.GetPointData().GetScalars()
1224            if vscalars is None or lut is None:
1225                # create a constant array
1226                arr = np.zeros([self.npoints, 4], dtype=np.uint8)
1227                col = np.array(self.properties.GetColor())
1228                col = np.round(col * 255).astype(np.uint8)
1229                alf = self.properties.GetOpacity()
1230                alf = np.round(alf * 255).astype(np.uint8)
1231                arr[:, (0, 1, 2)] = col
1232                arr[:, 3] = alf
1233            else:
1234                cols = lut.MapScalars(vscalars, 0, 0)
1235                arr = utils.vtk2numpy(cols)
1236            self.pointdata["PointsRGBA"] = arr
1237        self.pointdata.select("PointsRGBA")
1238        return self.pointdata["PointsRGBA"]
1239
1240    @pointcolors.setter
1241    def pointcolors(self, value):
1242        if isinstance(value, str):
1243            c = colors.get_color(value)
1244            value = np.array([*c, 1]) * 255
1245            value = np.round(value)
1246
1247        value = np.asarray(value)
1248        n = self.npoints
1249
1250        if value.ndim == 1:
1251            value = np.repeat([value], n, axis=0)
1252
1253        if value.shape[1] == 3:
1254            z = np.zeros((n, 1), dtype=np.uint8)
1255            value = np.append(value, z + 255, axis=1)
1256
1257        assert n == value.shape[0]
1258
1259        self.pointdata["PointsRGBA"] = value.astype(np.uint8)
1260        # self.mapper.SetColorModeToDirectScalars() # done in select()
1261        self.pointdata.select("PointsRGBA")
1262
1263    #####################################################################################
1264    def cmap(
1265        self,
1266        input_cmap,
1267        input_array=None,
1268        on="",
1269        name="Scalars",
1270        vmin=None,
1271        vmax=None,
1272        n_colors=256,
1273        alpha=1.0,
1274        logscale=False,
1275    ) -> Self:
1276        """
1277        Set individual point/cell colors by providing a list of scalar values and a color map.
1278
1279        Arguments:
1280            input_cmap : (str, list, vtkLookupTable, matplotlib.colors.LinearSegmentedColormap)
1281                color map scheme to transform a real number into a color.
1282            input_array : (str, list, vtkArray)
1283                can be the string name of an existing array, a new array or a `vtkArray`.
1284            on : (str)
1285                either 'points' or 'cells' or blank (automatic).
1286                Apply the color map to data which is defined on either points or cells.
1287            name : (str)
1288                give a name to the provided array (if input_array is an array)
1289            vmin : (float)
1290                clip scalars to this minimum value
1291            vmax : (float)
1292                clip scalars to this maximum value
1293            n_colors : (int)
1294                number of distinct colors to be used in colormap table.
1295            alpha : (float, list)
1296                Mesh transparency. Can be a `list` of values one for each vertex.
1297            logscale : (bool)
1298                Use logscale
1299
1300        Examples:
1301            - [mesh_coloring.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mesh_coloring.py)
1302            - [mesh_alphas.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mesh_alphas.py)
1303            - [mesh_custom.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mesh_custom.py)
1304            - (and many others)
1305
1306                ![](https://vedo.embl.es/images/basic/mesh_custom.png)
1307        """
1308        self._cmap_name = input_cmap
1309
1310        if on == "":
1311            try:
1312                on = self.mapper.GetScalarModeAsString().replace("Use", "")
1313                if on not in ["PointData", "CellData"]: # can be "Default"
1314                    on = "points"
1315                    self.mapper.SetScalarModeToUsePointData()
1316            except AttributeError:
1317                on = "points"
1318        elif on == "Default":
1319            on = "points"
1320            self.mapper.SetScalarModeToUsePointData()
1321
1322        if input_array is None:
1323            if not self.pointdata.keys() and self.celldata.keys():
1324                on = "cells"
1325                if not self.dataset.GetCellData().GetScalars():
1326                    input_array = 0  # pick the first at hand
1327
1328        if "point" in on.lower():
1329            data = self.dataset.GetPointData()
1330            n = self.dataset.GetNumberOfPoints()
1331        elif "cell" in on.lower():
1332            data = self.dataset.GetCellData()
1333            n = self.dataset.GetNumberOfCells()
1334        else:
1335            vedo.logger.error(
1336                f"Must specify in cmap(on=...) to either 'cells' or 'points', not {on}")
1337            raise RuntimeError()
1338
1339        if input_array is None:  # if None try to fetch the active scalars
1340            arr = data.GetScalars()
1341            if not arr:
1342                vedo.logger.error(f"in cmap(), cannot find any {on} active array ...skip coloring.")
1343                return self
1344
1345            if not arr.GetName():  # sometimes arrays dont have a name..
1346                arr.SetName(name)
1347
1348        elif isinstance(input_array, str):  # if a string is passed
1349            arr = data.GetArray(input_array)
1350            if not arr:
1351                vedo.logger.error(f"in cmap(), cannot find {on} array {input_array} ...skip coloring.")
1352                return self
1353
1354        elif isinstance(input_array, int):  # if an int is passed
1355            if input_array < data.GetNumberOfArrays():
1356                arr = data.GetArray(input_array)
1357            else:
1358                vedo.logger.error(f"in cmap(), cannot find {on} array at {input_array} ...skip coloring.")
1359                return self
1360
1361        elif utils.is_sequence(input_array):  # if a numpy array is passed
1362            npts = len(input_array)
1363            if npts != n:
1364                vedo.logger.error(f"in cmap(), nr. of input {on} scalars {npts} != {n} ...skip coloring.")
1365                return self
1366            arr = utils.numpy2vtk(input_array, name=name, dtype=float)
1367            data.AddArray(arr)
1368            data.Modified()
1369
1370        elif isinstance(input_array, vtki.vtkArray):  # if a vtkArray is passed
1371            arr = input_array
1372            data.AddArray(arr)
1373            data.Modified()
1374
1375        else:
1376            vedo.logger.error(f"in cmap(), cannot understand input type {type(input_array)}")
1377            raise RuntimeError()
1378
1379        # Now we have array "arr"
1380        array_name = arr.GetName()
1381
1382        if arr.GetNumberOfComponents() == 1:
1383            if vmin is None:
1384                vmin = arr.GetRange()[0]
1385            if vmax is None:
1386                vmax = arr.GetRange()[1]
1387        else:
1388            if vmin is None or vmax is None:
1389                vn = utils.mag(utils.vtk2numpy(arr))
1390            if vmin is None:
1391                vmin = vn.min()
1392            if vmax is None:
1393                vmax = vn.max()
1394
1395        # interpolate alphas if they are not constant
1396        if not utils.is_sequence(alpha):
1397            alpha = [alpha] * n_colors
1398        else:
1399            v = np.linspace(0, 1, n_colors, endpoint=True)
1400            xp = np.linspace(0, 1, len(alpha), endpoint=True)
1401            alpha = np.interp(v, xp, alpha)
1402
1403        ########################### build the look-up table
1404        if isinstance(input_cmap, vtki.vtkLookupTable):  # vtkLookupTable
1405            lut = input_cmap
1406
1407        elif utils.is_sequence(input_cmap):  # manual sequence of colors
1408            lut = vtki.vtkLookupTable()
1409            if logscale:
1410                lut.SetScaleToLog10()
1411            lut.SetRange(vmin, vmax)
1412            ncols = len(input_cmap)
1413            lut.SetNumberOfTableValues(ncols)
1414
1415            for i, c in enumerate(input_cmap):
1416                r, g, b = colors.get_color(c)
1417                lut.SetTableValue(i, r, g, b, alpha[i])
1418            lut.Build()
1419
1420        else:  
1421            # assume string cmap name OR matplotlib.colors.LinearSegmentedColormap
1422            lut = vtki.vtkLookupTable()
1423            if logscale:
1424                lut.SetScaleToLog10()
1425            lut.SetVectorModeToMagnitude()
1426            lut.SetRange(vmin, vmax)
1427            lut.SetNumberOfTableValues(n_colors)
1428            mycols = colors.color_map(range(n_colors), input_cmap, 0, n_colors)
1429            for i, c in enumerate(mycols):
1430                r, g, b = c
1431                lut.SetTableValue(i, r, g, b, alpha[i])
1432            lut.Build()
1433
1434        # TEST NEW WAY
1435        self.mapper.SetLookupTable(lut)
1436        self.mapper.ScalarVisibilityOn()
1437        self.mapper.SetColorModeToMapScalars()
1438        self.mapper.SetScalarRange(lut.GetRange())
1439        if "point" in on.lower():
1440            self.pointdata.select(array_name)
1441        else:
1442            self.celldata.select(array_name)
1443        return self
1444
1445        # # TEST this is the old way:
1446        # # arr.SetLookupTable(lut) # wrong! causes weird instabilities with LUT
1447        # # if data.GetScalars():
1448        # #     data.GetScalars().SetLookupTable(lut)
1449        # #     data.GetScalars().Modified()
1450
1451        # data.SetActiveScalars(array_name)
1452        # # data.SetScalars(arr)  # wrong! it deletes array in position 0, never use SetScalars
1453        # # data.SetActiveAttribute(array_name, 0) # boh!
1454
1455        # self.mapper.SetLookupTable(lut)
1456        # self.mapper.SetColorModeToMapScalars()  # so we dont need to convert uint8 scalars
1457
1458        # self.mapper.ScalarVisibilityOn()
1459        # self.mapper.SetScalarRange(lut.GetRange())
1460
1461        # if on.startswith("point"):
1462        #     self.mapper.SetScalarModeToUsePointData()
1463        # else:
1464        #     self.mapper.SetScalarModeToUseCellData()
1465        # if hasattr(self.mapper, "SetArrayName"):
1466        #     self.mapper.SetArrayName(array_name)
1467        # return self
1468
1469    def add_trail(self, offset=(0, 0, 0), n=50, c=None, alpha=1.0, lw=2) -> Self:
1470        """
1471        Add a trailing line to mesh.
1472        This new mesh is accessible through `mesh.trail`.
1473
1474        Arguments:
1475            offset : (float)
1476                set an offset vector from the object center.
1477            n : (int)
1478                number of segments
1479            lw : (float)
1480                line width of the trail
1481
1482        Examples:
1483            - [trail.py](https://github.com/marcomusy/vedo/tree/master/examples/simulations/trail.py)
1484
1485                ![](https://vedo.embl.es/images/simulations/trail.gif)
1486
1487            - [airplane1.py](https://github.com/marcomusy/vedo/tree/master/examples/simulations/airplane1.py)
1488            - [airplane2.py](https://github.com/marcomusy/vedo/tree/master/examples/simulations/airplane2.py)
1489        """
1490        if self.trail is None:
1491            pos = self.pos()
1492            self.trail_offset = np.asarray(offset)
1493            self.trail_points = [pos] * n
1494
1495            if c is None:
1496                col = self.properties.GetColor()
1497            else:
1498                col = colors.get_color(c)
1499
1500            tline = vedo.shapes.Line(pos, pos, res=n, c=col, alpha=alpha, lw=lw)
1501            self.trail = tline  # holds the Line
1502        return self
1503
1504    def update_trail(self) -> Self:
1505        """
1506        Update the trailing line of a moving object.
1507        """
1508        currentpos = self.pos()
1509        self.trail_points.append(currentpos)  # cycle
1510        self.trail_points.pop(0)
1511        data = np.array(self.trail_points) + self.trail_offset
1512        tpoly = self.trail.dataset
1513        tpoly.GetPoints().SetData(utils.numpy2vtk(data, dtype=np.float32))
1514        return self
1515
1516    def _compute_shadow(self, plane, point, direction):
1517        shad = self.clone()
1518        shad.name = "Shadow"
1519
1520        tarr = shad.dataset.GetPointData().GetTCoords()
1521        if tarr:  # remove any texture coords
1522            tname = tarr.GetName()
1523            shad.pointdata.remove(tname)
1524            shad.dataset.GetPointData().SetTCoords(None)
1525            shad.actor.SetTexture(None)
1526
1527        pts = shad.vertices
1528        if plane == "x":
1529            # shad = shad.project_on_plane('x')
1530            # instead do it manually so in case of alpha<1
1531            # we dont see glitches due to coplanar points
1532            # we leave a small tolerance of 0.1% in thickness
1533            x0, x1 = self.xbounds()
1534            pts[:, 0] = (pts[:, 0] - (x0 + x1) / 2) / 1000 + self.actor.GetOrigin()[0]
1535            shad.vertices = pts
1536            shad.x(point)
1537        elif plane == "y":
1538            x0, x1 = self.ybounds()
1539            pts[:, 1] = (pts[:, 1] - (x0 + x1) / 2) / 1000 + self.actor.GetOrigin()[1]
1540            shad.vertices = pts
1541            shad.y(point)
1542        elif plane == "z":
1543            x0, x1 = self.zbounds()
1544            pts[:, 2] = (pts[:, 2] - (x0 + x1) / 2) / 1000 + self.actor.GetOrigin()[2]
1545            shad.vertices = pts
1546            shad.z(point)
1547        else:
1548            shad = shad.project_on_plane(plane, point, direction)
1549        return shad
1550
1551    def add_shadow(self, plane, point, direction=None, c=(0.6, 0.6, 0.6), alpha=1, culling=0) -> Self:
1552        """
1553        Generate a shadow out of an `Mesh` on one of the three Cartesian planes.
1554        The output is a new `Mesh` representing the shadow.
1555        This new mesh is accessible through `mesh.shadow`.
1556        By default the shadow mesh is placed on the bottom wall of the bounding box.
1557
1558        See also `pointcloud.project_on_plane()`.
1559
1560        Arguments:
1561            plane : (str, Plane)
1562                if plane is `str`, plane can be one of `['x', 'y', 'z']`,
1563                represents x-plane, y-plane and z-plane, respectively.
1564                Otherwise, plane should be an instance of `vedo.shapes.Plane`
1565            point : (float, array)
1566                if plane is `str`, point should be a float represents the intercept.
1567                Otherwise, point is the camera point of perspective projection
1568            direction : (list)
1569                direction of oblique projection
1570            culling : (int)
1571                choose between front [1] or backface [-1] culling or None.
1572
1573        Examples:
1574            - [shadow1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/shadow1.py)
1575            - [airplane1.py](https://github.com/marcomusy/vedo/tree/master/examples/simulations/airplane1.py)
1576            - [airplane2.py](https://github.com/marcomusy/vedo/tree/master/examples/simulations/airplane2.py)
1577
1578            ![](https://vedo.embl.es/images/simulations/57341963-b8910900-713c-11e9-898a-84b6d3712bce.gif)
1579        """
1580        shad = self._compute_shadow(plane, point, direction)
1581        shad.c(c).alpha(alpha)
1582
1583        try:
1584            # Points dont have these methods
1585            shad.flat()
1586            if culling in (1, True):
1587                shad.frontface_culling()
1588            elif culling == -1:
1589                shad.backface_culling()
1590        except AttributeError:
1591            pass
1592
1593        shad.properties.LightingOff()
1594        shad.actor.SetPickable(False)
1595        shad.actor.SetUseBounds(True)
1596
1597        if shad not in self.shadows:
1598            self.shadows.append(shad)
1599            shad.info = dict(plane=plane, point=point, direction=direction)
1600            # shad.metadata["plane"] = plane
1601            # shad.metadata["point"] = point
1602            # print("AAAA", direction, plane, point)
1603            # if direction is None:
1604            #     direction = [0,0,0]
1605            # shad.metadata["direction"] = direction
1606        return self
1607
1608    def update_shadows(self) -> Self:
1609        """Update the shadows of a moving object."""
1610        for sha in self.shadows:
1611            plane = sha.info["plane"]
1612            point = sha.info["point"]
1613            direction = sha.info["direction"]
1614            # print("update_shadows direction", direction,plane,point )
1615            # plane = sha.metadata["plane"]
1616            # point = sha.metadata["point"]
1617            # direction = sha.metadata["direction"]
1618            # if direction[0]==0 and direction[1]==0 and direction[2]==0:
1619            #     direction = None
1620            # print("BBBB", sha.metadata["direction"], 
1621            #       sha.metadata["plane"], sha.metadata["point"])
1622            new_sha = self._compute_shadow(plane, point, direction)
1623            sha._update(new_sha.dataset)
1624        if self.trail:
1625            self.trail.update_shadows()
1626        return self
1627
1628    def labels(
1629        self,
1630        content=None,
1631        on="points",
1632        scale=None,
1633        xrot=0.0,
1634        yrot=0.0,
1635        zrot=0.0,
1636        ratio=1,
1637        precision=None,
1638        italic=False,
1639        font="",
1640        justify="",
1641        c="black",
1642        alpha=1.0,
1643    ) -> Union["vedo.Mesh", None]:
1644        """
1645        Generate value or ID labels for mesh cells or points.
1646        For large nr. of labels use `font="VTK"` which is much faster.
1647
1648        See also:
1649            `labels2d()`, `flagpole()`, `caption()` and `legend()`.
1650
1651        Arguments:
1652            content : (list,int,str)
1653                either 'id', 'cellid', array name or array number.
1654                A array can also be passed (must match the nr. of points or cells).
1655            on : (str)
1656                generate labels for "cells" instead of "points"
1657            scale : (float)
1658                absolute size of labels, if left as None it is automatic
1659            zrot : (float)
1660                local rotation angle of label in degrees
1661            ratio : (int)
1662                skipping ratio, to reduce nr of labels for large meshes
1663            precision : (int)
1664                numeric precision of labels
1665
1666        ```python
1667        from vedo import *
1668        s = Sphere(res=10).linewidth(1).c("orange").compute_normals()
1669        point_ids = s.labels('id', on="points").c('green')
1670        cell_ids  = s.labels('id', on="cells" ).c('black')
1671        show(s, point_ids, cell_ids)
1672        ```
1673        ![](https://vedo.embl.es/images/feats/labels.png)
1674
1675        Examples:
1676            - [boundaries.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/boundaries.py)
1677
1678                ![](https://vedo.embl.es/images/basic/boundaries.png)
1679        """
1680        
1681        cells = False
1682        if "cell" in on or "face" in on:
1683            cells = True
1684            justify = "centered" if justify == "" else justify
1685
1686        if isinstance(content, str):
1687            if content in ("pointid", "pointsid"):
1688                cells = False
1689                content = "id"
1690                justify = "bottom-left" if justify == "" else justify
1691            if content in ("cellid", "cellsid"):
1692                cells = True
1693                content = "id"
1694                justify = "centered" if justify == "" else justify
1695
1696        try:
1697            if cells:
1698                ns = np.sqrt(self.ncells)
1699                elems = self.cell_centers
1700                norms = self.cell_normals
1701                justify = "centered" if justify == "" else justify
1702            else:
1703                ns = np.sqrt(self.npoints)
1704                elems = self.vertices
1705                norms = self.vertex_normals
1706        except AttributeError:
1707            norms = []
1708        
1709        if not justify:
1710            justify = "bottom-left"
1711
1712        hasnorms = False
1713        if len(norms) > 0:
1714            hasnorms = True
1715
1716        if scale is None:
1717            if not ns:
1718                ns = 100
1719            scale = self.diagonal_size() / ns / 10
1720
1721        arr = None
1722        mode = 0
1723        if content is None:
1724            mode = 0
1725            if cells:
1726                if self.dataset.GetCellData().GetScalars():
1727                    name = self.dataset.GetCellData().GetScalars().GetName()
1728                    arr = self.celldata[name]
1729            else:
1730                if self.dataset.GetPointData().GetScalars():
1731                    name = self.dataset.GetPointData().GetScalars().GetName()
1732                    arr = self.pointdata[name]
1733        elif isinstance(content, (str, int)):
1734            if content == "id":
1735                mode = 1
1736            elif cells:
1737                mode = 0
1738                arr = self.celldata[content]
1739            else:
1740                mode = 0
1741                arr = self.pointdata[content]
1742        elif utils.is_sequence(content):
1743            mode = 0
1744            arr = content
1745
1746        if arr is None and mode == 0:
1747            vedo.logger.error("in labels(), array not found in point or cell data")
1748            return None
1749
1750        ratio = int(ratio+0.5)
1751        tapp = vtki.new("AppendPolyData")
1752        has_inputs = False
1753
1754        for i, e in enumerate(elems):
1755            if i % ratio:
1756                continue
1757
1758            if mode == 1:
1759                txt_lab = str(i)
1760            else:
1761                if precision:
1762                    txt_lab = utils.precision(arr[i], precision)
1763                else:
1764                    txt_lab = str(arr[i])
1765
1766            if not txt_lab:
1767                continue
1768
1769            if font == "VTK":
1770                tx = vtki.new("VectorText")
1771                tx.SetText(txt_lab)
1772                tx.Update()
1773                tx_poly = tx.GetOutput()
1774            else:
1775                tx_poly = vedo.shapes.Text3D(txt_lab, font=font, justify=justify).dataset
1776
1777            if tx_poly.GetNumberOfPoints() == 0:
1778                continue  ######################
1779
1780            T = vtki.vtkTransform()
1781            T.PostMultiply()
1782            if italic:
1783                T.Concatenate([1, 0.2, 0, 0,
1784                               0, 1  , 0, 0,
1785                               0, 0  , 1, 0,
1786                               0, 0  , 0, 1])
1787            if hasnorms:
1788                ni = norms[i]
1789                if cells and font=="VTK":  # center-justify
1790                    bb = tx_poly.GetBounds()
1791                    dx, dy = (bb[1] - bb[0]) / 2, (bb[3] - bb[2]) / 2
1792                    T.Translate(-dx, -dy, 0)
1793                if xrot: T.RotateX(xrot)
1794                if yrot: T.RotateY(yrot)
1795                if zrot: T.RotateZ(zrot)
1796                crossvec = np.cross([0, 0, 1], ni)
1797                angle = np.arccos(np.dot([0, 0, 1], ni)) * 57.3
1798                T.RotateWXYZ(float(angle), crossvec.tolist())
1799                T.Translate(ni / 100)
1800            else:
1801                if xrot: T.RotateX(xrot)
1802                if yrot: T.RotateY(yrot)
1803                if zrot: T.RotateZ(zrot)
1804            T.Scale(scale, scale, scale)
1805            T.Translate(e)
1806            tf = vtki.new("TransformPolyDataFilter")
1807            tf.SetInputData(tx_poly)
1808            tf.SetTransform(T)
1809            tf.Update()
1810            tapp.AddInputData(tf.GetOutput())
1811            has_inputs = True
1812
1813        if has_inputs:
1814            tapp.Update()
1815            lpoly = tapp.GetOutput()
1816        else:
1817            lpoly = vtki.vtkPolyData()
1818        ids = vedo.Mesh(lpoly, c=c, alpha=alpha)
1819        ids.properties.LightingOff()
1820        ids.actor.PickableOff()
1821        ids.actor.SetUseBounds(False)
1822        ids.name = "Labels"
1823        return ids
1824
1825    def labels2d(
1826        self,
1827        content="id",
1828        on="points",
1829        scale=1.0,
1830        precision=4,
1831        font="Calco",
1832        justify="bottom-left",
1833        angle=0.0,
1834        frame=False,
1835        c="black",
1836        bc=None,
1837        alpha=1.0,
1838    ) -> Union["Actor2D", None]:
1839        """
1840        Generate value or ID bi-dimensional labels for mesh cells or points.
1841
1842        See also: `labels()`, `flagpole()`, `caption()` and `legend()`.
1843
1844        Arguments:
1845            content : (str)
1846                either 'id', 'cellid', or array name
1847            on : (str)
1848                generate labels for "cells" instead of "points" (the default)
1849            scale : (float)
1850                size scaling of labels
1851            precision : (int)
1852                precision of numeric labels
1853            angle : (float)
1854                local rotation angle of label in degrees
1855            frame : (bool)
1856                draw a frame around the label
1857            bc : (str)
1858                background color of the label
1859
1860        ```python
1861        from vedo import Sphere, show
1862        sph = Sphere(quads=True, res=4).compute_normals().wireframe()
1863        sph.celldata["zvals"] = sph.cell_centers[:,2]
1864        l2d = sph.labels("zvals", on="cells", precision=2).backcolor('orange9')
1865        show(sph, l2d, axes=1).close()
1866        ```
1867        ![](https://vedo.embl.es/images/feats/labels2d.png)
1868        """
1869        cells = False
1870        if "cell" in on:
1871            cells = True
1872
1873        if isinstance(content, str):
1874            if content in ("id", "pointid", "pointsid"):
1875                cells = False
1876                content = "id"
1877            if content in ("cellid", "cellsid"):
1878                cells = True
1879                content = "id"
1880
1881        if cells:
1882            if content != "id" and content not in self.celldata.keys():
1883                vedo.logger.error(f"In labels2d: cell array {content} does not exist.")
1884                return None
1885            cellcloud = vedo.Points(self.cell_centers)
1886            arr = self.dataset.GetCellData().GetScalars()
1887            poly = cellcloud.dataset
1888            poly.GetPointData().SetScalars(arr)
1889        else:
1890            poly = self.dataset
1891            if content != "id" and content not in self.pointdata.keys():
1892                vedo.logger.error(f"In labels2d: point array {content} does not exist.")
1893                return None
1894
1895        mp = vtki.new("LabeledDataMapper")
1896
1897        if content == "id":
1898            mp.SetLabelModeToLabelIds()
1899        else:
1900            mp.SetLabelModeToLabelScalars()
1901            if precision is not None:
1902                mp.SetLabelFormat(f"%-#.{precision}g")
1903
1904        pr = mp.GetLabelTextProperty()
1905        c = colors.get_color(c)
1906        pr.SetColor(c)
1907        pr.SetOpacity(alpha)
1908        pr.SetFrame(frame)
1909        pr.SetFrameColor(c)
1910        pr.SetItalic(False)
1911        pr.BoldOff()
1912        pr.ShadowOff()
1913        pr.UseTightBoundingBoxOn()
1914        pr.SetOrientation(angle)
1915        pr.SetFontFamily(vtki.VTK_FONT_FILE)
1916        fl = utils.get_font_path(font)
1917        pr.SetFontFile(fl)
1918        pr.SetFontSize(int(20 * scale))
1919
1920        if "cent" in justify or "mid" in justify:
1921            pr.SetJustificationToCentered()
1922        elif "rig" in justify:
1923            pr.SetJustificationToRight()
1924        elif "left" in justify:
1925            pr.SetJustificationToLeft()
1926        # ------
1927        if "top" in justify:
1928            pr.SetVerticalJustificationToTop()
1929        else:
1930            pr.SetVerticalJustificationToBottom()
1931
1932        if bc is not None:
1933            bc = colors.get_color(bc)
1934            pr.SetBackgroundColor(bc)
1935            pr.SetBackgroundOpacity(alpha)
1936
1937        mp.SetInputData(poly)
1938        a2d = Actor2D()
1939        a2d.PickableOff()
1940        a2d.SetMapper(mp)
1941        return a2d
1942
1943    def legend(self, txt) -> Self:
1944        """Book a legend text."""
1945        self.info["legend"] = txt
1946        # self.metadata["legend"] = txt
1947        return self
1948
1949    def flagpole(
1950        self,
1951        txt=None,
1952        point=None,
1953        offset=None,
1954        s=None,
1955        font="Calco",
1956        rounded=True,
1957        c=None,
1958        alpha=1.0,
1959        lw=2,
1960        italic=0.0,
1961        padding=0.1,
1962    ) -> Union["vedo.Mesh", None]:
1963        """
1964        Generate a flag pole style element to describe an object.
1965        Returns a `Mesh` object.
1966
1967        Use flagpole.follow_camera() to make it face the camera in the scene.
1968
1969        Consider using `settings.use_parallel_projection = True` 
1970        to avoid perspective distortions.
1971
1972        See also `flagpost()`.
1973
1974        Arguments:
1975            txt : (str)
1976                Text to display. The default is the filename or the object name.
1977            point : (list)
1978                position of the flagpole pointer. 
1979            offset : (list)
1980                text offset wrt the application point. 
1981            s : (float)
1982                size of the flagpole.
1983            font : (str)
1984                font face. Check [available fonts here](https://vedo.embl.es/fonts).
1985            rounded : (bool)
1986                draw a rounded or squared box around the text.
1987            c : (list)
1988                text and box color.
1989            alpha : (float)
1990                opacity of text and box.
1991            lw : (float)
1992                line with of box frame.
1993            italic : (float)
1994                italicness of text.
1995
1996        Examples:
1997            - [intersect2d.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/intersect2d.py)
1998
1999                ![](https://vedo.embl.es/images/pyplot/intersect2d.png)
2000
2001            - [goniometer.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/goniometer.py)
2002            - [flag_labels1.py](https://github.com/marcomusy/vedo/tree/master/examples/other/flag_labels1.py)
2003            - [flag_labels2.py](https://github.com/marcomusy/vedo/tree/master/examples/other/flag_labels2.py)
2004        """
2005        objs = []
2006
2007        if txt is None:
2008            if self.filename:
2009                txt = self.filename.split("/")[-1]
2010            elif self.name:
2011                txt = self.name
2012            else:
2013                return None
2014
2015        x0, x1, y0, y1, z0, z1 = self.bounds()
2016        d = self.diagonal_size()
2017        if point is None:
2018            if d:
2019                point = self.closest_point([(x0 + x1) / 2, (y0 + y1) / 2, z1])
2020                # point = self.closest_point([x1, y0, z1])
2021            else:  # it's a Point
2022                point = self.transform.position
2023
2024        pt = utils.make3d(point)
2025
2026        if offset is None:
2027            offset = [(x1 - x0) / 1.75, (y1 - y0) / 5, 0]
2028        offset = utils.make3d(offset)
2029
2030        if s is None:
2031            s = d / 20
2032
2033        sph = None
2034        if d and (z1 - z0) / d > 0.1:
2035            sph = vedo.shapes.Sphere(pt, r=s * 0.4, res=6)
2036
2037        if c is None:
2038            c = np.array(self.color()) / 1.4
2039
2040        lab = vedo.shapes.Text3D(
2041            txt, pos=pt + offset, s=s, font=font, italic=italic, justify="center"
2042        )
2043        objs.append(lab)
2044
2045        if d and not sph:
2046            sph = vedo.shapes.Circle(pt, r=s / 3, res=16)
2047        objs.append(sph)
2048
2049        x0, x1, y0, y1, z0, z1 = lab.bounds()
2050        aline = [(x0,y0,z0), (x1,y0,z0), (x1,y1,z0), (x0,y1,z0)]
2051        if rounded:
2052            box = vedo.shapes.KSpline(aline, closed=True)
2053        else:
2054            box = vedo.shapes.Line(aline, closed=True)
2055
2056        cnt = [(x0 + x1) / 2, (y0 + y1) / 2, (z0 + z1) / 2]
2057
2058        # box.actor.SetOrigin(cnt)
2059        box.scale([1 + padding, 1 + 2 * padding, 1], origin=cnt)
2060        objs.append(box)
2061
2062        x0, x1, y0, y1, z0, z1 = box.bounds()
2063        if x0 < pt[0] < x1:
2064            c0 = box.closest_point(pt)
2065            c1 = [c0[0], c0[1] + (pt[1] - y0) / 4, pt[2]]
2066        elif (pt[0] - x0) < (x1 - pt[0]):
2067            c0 = [x0, (y0 + y1) / 2, pt[2]]
2068            c1 = [x0 + (pt[0] - x0) / 4, (y0 + y1) / 2, pt[2]]
2069        else:
2070            c0 = [x1, (y0 + y1) / 2, pt[2]]
2071            c1 = [x1 + (pt[0] - x1) / 4, (y0 + y1) / 2, pt[2]]
2072
2073        con = vedo.shapes.Line([c0, c1, pt])
2074        objs.append(con)
2075
2076        mobjs = vedo.merge(objs).c(c).alpha(alpha)
2077        mobjs.name = "FlagPole"
2078        mobjs.bc("tomato").pickable(False)
2079        mobjs.properties.LightingOff()
2080        mobjs.properties.SetLineWidth(lw)
2081        mobjs.actor.UseBoundsOff()
2082        mobjs.actor.SetPosition([0,0,0])
2083        mobjs.actor.SetOrigin(pt)
2084        return mobjs
2085
2086        # mobjs = vedo.Assembly(objs)#.c(c).alpha(alpha)
2087        # mobjs.name = "FlagPole"
2088        # # mobjs.bc("tomato").pickable(False)
2089        # # mobjs.properties.LightingOff()
2090        # # mobjs.properties.SetLineWidth(lw)
2091        # # mobjs.actor.UseBoundsOff()
2092        # # mobjs.actor.SetPosition([0,0,0])
2093        # # mobjs.actor.SetOrigin(pt)
2094        # # print(pt)
2095        # return mobjs
2096
2097    def flagpost(
2098        self,
2099        txt=None,
2100        point=None,
2101        offset=None,
2102        s=1.0,
2103        c="k9",
2104        bc="k1",
2105        alpha=1,
2106        lw=0,
2107        font="Calco",
2108        justify="center-left",
2109        vspacing=1.0,
2110    ) -> Union["vedo.addons.Flagpost", None]:
2111        """
2112        Generate a flag post style element to describe an object.
2113
2114        Arguments:
2115            txt : (str)
2116                Text to display. The default is the filename or the object name.
2117            point : (list)
2118                position of the flag anchor point. The default is None.
2119            offset : (list)
2120                a 3D displacement or offset. The default is None.
2121            s : (float)
2122                size of the text to be shown
2123            c : (list)
2124                color of text and line
2125            bc : (list)
2126                color of the flag background
2127            alpha : (float)
2128                opacity of text and box.
2129            lw : (int)
2130                line with of box frame. The default is 0.
2131            font : (str)
2132                font name. Use a monospace font for better rendering. The default is "Calco".
2133                Type `vedo -r fonts` for a font demo.
2134                Check [available fonts here](https://vedo.embl.es/fonts).
2135            justify : (str)
2136                internal text justification. The default is "center-left".
2137            vspacing : (float)
2138                vertical spacing between lines.
2139
2140        Examples:
2141            - [flag_labels2.py](https://github.com/marcomusy/vedo/tree/master/examples/examples/other/flag_labels2.py)
2142
2143            ![](https://vedo.embl.es/images/other/flag_labels2.png)
2144        """
2145        if txt is None:
2146            if self.filename:
2147                txt = self.filename.split("/")[-1]
2148            elif self.name:
2149                txt = self.name
2150            else:
2151                return None
2152
2153        x0, x1, y0, y1, z0, z1 = self.bounds()
2154        d = self.diagonal_size()
2155        if point is None:
2156            if d:
2157                point = self.closest_point([(x0 + x1) / 2, (y0 + y1) / 2, z1])
2158            else:  # it's a Point
2159                point = self.transform.position
2160
2161        point = utils.make3d(point)
2162
2163        if offset is None:
2164            offset = [0, 0, (z1 - z0) / 2]
2165        offset = utils.make3d(offset)
2166
2167        fpost = vedo.addons.Flagpost(
2168            txt, point, point + offset, s, c, bc, alpha, lw, font, justify, vspacing
2169        )
2170        self._caption = fpost
2171        return fpost
2172
2173    def caption(
2174        self,
2175        txt=None,
2176        point=None,
2177        size=(0.30, 0.15),
2178        padding=5,
2179        font="Calco",
2180        justify="center-right",
2181        vspacing=1.0,
2182        c=None,
2183        alpha=1.0,
2184        lw=1,
2185        ontop=True,
2186    ) -> Union["vtki.vtkCaptionActor2D", None]:
2187        """
2188        Create a 2D caption to an object which follows the camera movements.
2189        Latex is not supported. Returns the same input object for concatenation.
2190
2191        See also `flagpole()`, `flagpost()`, `labels()` and `legend()`
2192        with similar functionality.
2193
2194        Arguments:
2195            txt : (str)
2196                text to be rendered. The default is the file name.
2197            point : (list)
2198                anchoring point. The default is None.
2199            size : (list)
2200                (width, height) of the caption box. The default is (0.30, 0.15).
2201            padding : (float)
2202                padding space of the caption box in pixels. The default is 5.
2203            font : (str)
2204                font name. Use a monospace font for better rendering. The default is "VictorMono".
2205                Type `vedo -r fonts` for a font demo.
2206                Check [available fonts here](https://vedo.embl.es/fonts).
2207            justify : (str)
2208                internal text justification. The default is "center-right".
2209            vspacing : (float)
2210                vertical spacing between lines. The default is 1.
2211            c : (str)
2212                text and box color. The default is 'lb'.
2213            alpha : (float)
2214                text and box transparency. The default is 1.
2215            lw : (int)
2216                line width in pixels. The default is 1.
2217            ontop : (bool)
2218                keep the 2d caption always on top. The default is True.
2219
2220        Examples:
2221            - [caption.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/caption.py)
2222
2223                ![](https://vedo.embl.es/images/pyplot/caption.png)
2224
2225            - [flag_labels1.py](https://github.com/marcomusy/vedo/tree/master/examples/other/flag_labels1.py)
2226            - [flag_labels2.py](https://github.com/marcomusy/vedo/tree/master/examples/other/flag_labels2.py)
2227        """
2228        if txt is None:
2229            if self.filename:
2230                txt = self.filename.split("/")[-1]
2231            elif self.name:
2232                txt = self.name
2233
2234        if not txt:  # disable it
2235            self._caption = None
2236            return None
2237
2238        for r in vedo.shapes._reps:
2239            txt = txt.replace(r[0], r[1])
2240
2241        if c is None:
2242            c = np.array(self.properties.GetColor()) / 2
2243        else:
2244            c = colors.get_color(c)
2245
2246        if point is None:
2247            x0, x1, y0, y1, _, z1 = self.dataset.GetBounds()
2248            pt = [(x0 + x1) / 2, (y0 + y1) / 2, z1]
2249            point = self.closest_point(pt)
2250
2251        capt = vtki.vtkCaptionActor2D()
2252        capt.SetAttachmentPoint(point)
2253        capt.SetBorder(True)
2254        capt.SetLeader(True)
2255        sph = vtki.new("SphereSource")
2256        sph.Update()
2257        capt.SetLeaderGlyphData(sph.GetOutput())
2258        capt.SetMaximumLeaderGlyphSize(5)
2259        capt.SetPadding(int(padding))
2260        capt.SetCaption(txt)
2261        capt.SetWidth(size[0])
2262        capt.SetHeight(size[1])
2263        capt.SetThreeDimensionalLeader(not ontop)
2264
2265        pra = capt.GetProperty()
2266        pra.SetColor(c)
2267        pra.SetOpacity(alpha)
2268        pra.SetLineWidth(lw)
2269
2270        pr = capt.GetCaptionTextProperty()
2271        pr.SetFontFamily(vtki.VTK_FONT_FILE)
2272        fl = utils.get_font_path(font)
2273        pr.SetFontFile(fl)
2274        pr.ShadowOff()
2275        pr.BoldOff()
2276        pr.FrameOff()
2277        pr.SetColor(c)
2278        pr.SetOpacity(alpha)
2279        pr.SetJustificationToLeft()
2280        if "top" in justify:
2281            pr.SetVerticalJustificationToTop()
2282        if "bottom" in justify:
2283            pr.SetVerticalJustificationToBottom()
2284        if "cent" in justify:
2285            pr.SetVerticalJustificationToCentered()
2286            pr.SetJustificationToCentered()
2287        if "left" in justify:
2288            pr.SetJustificationToLeft()
2289        if "right" in justify:
2290            pr.SetJustificationToRight()
2291        pr.SetLineSpacing(vspacing)
2292        return capt
2293
2294
2295#####################################################################
2296class MeshVisual(PointsVisual):
2297    """Class to manage the visual aspects of a ``Maesh`` object."""
2298
2299    def __init__(self) -> None:
2300        # print("INIT MeshVisual", super())
2301        super().__init__()
2302
2303    def follow_camera(self, camera=None, origin=None) -> Self:
2304        """
2305        Return an object that will follow camera movements and stay locked to it.
2306        Use `mesh.follow_camera(False)` to disable it.
2307
2308        A `vtkCamera` object can also be passed.
2309        """
2310        if camera is False:
2311            try:
2312                self.SetCamera(None)
2313                return self
2314            except AttributeError:
2315                return self
2316
2317        factor = vtki.vtkFollower()
2318        factor.SetMapper(self.mapper)
2319        factor.SetProperty(self.properties)
2320        factor.SetBackfaceProperty(self.actor.GetBackfaceProperty())
2321        factor.SetTexture(self.actor.GetTexture())
2322        factor.SetScale(self.actor.GetScale())
2323        # factor.SetOrientation(self.actor.GetOrientation())
2324        factor.SetPosition(self.actor.GetPosition())
2325        factor.SetUseBounds(self.actor.GetUseBounds())
2326
2327        if origin is None:
2328            factor.SetOrigin(self.actor.GetOrigin())
2329        else:
2330            factor.SetOrigin(origin)
2331
2332        factor.PickableOff()
2333
2334        if isinstance(camera, vtki.vtkCamera):
2335            factor.SetCamera(camera)
2336        else:
2337            plt = vedo.plotter_instance
2338            if plt and plt.renderer and plt.renderer.GetActiveCamera():
2339                factor.SetCamera(plt.renderer.GetActiveCamera())
2340
2341        self.actor = None
2342        factor.retrieve_object = weak_ref_to(self)
2343        self.actor = factor
2344        return self
2345
2346    def wireframe(self, value=True) -> Self:
2347        """Set mesh's representation as wireframe or solid surface."""
2348        if value:
2349            self.properties.SetRepresentationToWireframe()
2350        else:
2351            self.properties.SetRepresentationToSurface()
2352        return self
2353
2354    def flat(self)  -> Self:
2355        """Set surface interpolation to flat.
2356
2357        <img src="https://upload.wikimedia.org/wikipedia/commons/6/6b/Phong_components_version_4.png" width="700">
2358        """
2359        self.properties.SetInterpolationToFlat()
2360        return self
2361
2362    def phong(self) -> Self:
2363        """Set surface interpolation to "phong"."""
2364        self.properties.SetInterpolationToPhong()
2365        return self
2366
2367    def backface_culling(self, value=True) -> Self:
2368        """Set culling of polygons based on orientation of normal with respect to camera."""
2369        self.properties.SetBackfaceCulling(value)
2370        return self
2371
2372    def render_lines_as_tubes(self, value=True) -> Self:
2373        """Wrap a fake tube around a simple line for visualization"""
2374        self.properties.SetRenderLinesAsTubes(value)
2375        return self
2376
2377    def frontface_culling(self, value=True) -> Self:
2378        """Set culling of polygons based on orientation of normal with respect to camera."""
2379        self.properties.SetFrontfaceCulling(value)
2380        return self
2381
2382    def backcolor(self, bc=None) -> Union[Self, np.ndarray]:
2383        """
2384        Set/get mesh's backface color.
2385        """
2386        back_prop = self.actor.GetBackfaceProperty()
2387
2388        if bc is None:
2389            if back_prop:
2390                return back_prop.GetDiffuseColor()
2391            return self
2392
2393        if self.properties.GetOpacity() < 1:
2394            return self
2395
2396        if not back_prop:
2397            back_prop = vtki.vtkProperty()
2398
2399        back_prop.SetDiffuseColor(colors.get_color(bc))
2400        back_prop.SetOpacity(self.properties.GetOpacity())
2401        self.actor.SetBackfaceProperty(back_prop)
2402        self.mapper.ScalarVisibilityOff()
2403        return self
2404
2405    def bc(self, backcolor=False) -> Union[Self, np.ndarray]:
2406        """Shortcut for `mesh.backcolor()`."""
2407        return self.backcolor(backcolor)
2408
2409    def linewidth(self, lw=None) -> Union[Self, int]:
2410        """Set/get width of mesh edges. Same as `lw()`."""
2411        if lw is not None:
2412            if lw == 0:
2413                self.properties.EdgeVisibilityOff()
2414                self.properties.SetRepresentationToSurface()
2415                return self
2416            self.properties.EdgeVisibilityOn()
2417            self.properties.SetLineWidth(lw)
2418        else:
2419            return self.properties.GetLineWidth()
2420        return self
2421
2422    def lw(self, linewidth=None) -> Union[Self, int]:
2423        """Set/get width of mesh edges. Same as `linewidth()`."""
2424        return self.linewidth(linewidth)
2425
2426    def linecolor(self, lc=None) -> Union[Self, np.ndarray]:
2427        """Set/get color of mesh edges. Same as `lc()`."""
2428        if lc is None:
2429            return np.array(self.properties.GetEdgeColor())
2430        self.properties.EdgeVisibilityOn()
2431        self.properties.SetEdgeColor(colors.get_color(lc))
2432        return self
2433
2434    def lc(self, linecolor=None) -> Union[Self, np.ndarray]:
2435        """Set/get color of mesh edges. Same as `linecolor()`."""
2436        return self.linecolor(linecolor)
2437
2438    def texture(
2439        self,
2440        tname,
2441        tcoords=None,
2442        interpolate=True,
2443        repeat=True,
2444        edge_clamp=False,
2445        scale=None,
2446        ushift=None,
2447        vshift=None,
2448    ) -> Self:
2449        """
2450        Assign a texture to mesh from image file or predefined texture `tname`.
2451        If tname is set to `None` texture is disabled.
2452        Input tname can also be an array or a `vtkTexture`.
2453
2454        Arguments:
2455            tname : (numpy.array, str, Image, vtkTexture, None)
2456                the input texture to be applied. Can be a numpy array, a path to an image file,
2457                a vedo Image. The None value disables texture.
2458            tcoords : (numpy.array, str)
2459                this is the (u,v) texture coordinate array. Can also be a string of an existing array
2460                in the mesh.
2461            interpolate : (bool)
2462                turn on/off linear interpolation of the texture map when rendering.
2463            repeat : (bool)
2464                repeat of the texture when tcoords extend beyond the [0,1] range.
2465            edge_clamp : (bool)
2466                turn on/off the clamping of the texture map when
2467                the texture coords extend beyond the [0,1] range.
2468                Only used when repeat is False, and edge clamping is supported by the graphics card.
2469            scale : (bool)
2470                scale the texture image by this factor
2471            ushift : (bool)
2472                shift u-coordinates of texture by this amount
2473            vshift : (bool)
2474                shift v-coordinates of texture by this amount
2475
2476        Examples:
2477            - [texturecubes.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/texturecubes.py)
2478
2479            ![](https://vedo.embl.es/images/basic/texturecubes.png)
2480        """
2481        pd = self.dataset
2482        out_img = None
2483
2484        if tname is None:  # disable texture
2485            pd.GetPointData().SetTCoords(None)
2486            pd.GetPointData().Modified()
2487            return self  ######################################
2488
2489        if isinstance(tname, vtki.vtkTexture):
2490            tu = tname
2491
2492        elif isinstance(tname, vedo.Image):
2493            tu = vtki.vtkTexture()
2494            out_img = tname.dataset
2495
2496        elif utils.is_sequence(tname):
2497            tu = vtki.vtkTexture()
2498            out_img = vedo.image._get_img(tname)
2499
2500        elif isinstance(tname, str):
2501            tu = vtki.vtkTexture()
2502
2503            if "https://" in tname:
2504                try:
2505                    tname = vedo.file_io.download(tname, verbose=False)
2506                except:
2507                    vedo.logger.error(f"texture {tname} could not be downloaded")
2508                    return self
2509
2510            fn = tname + ".jpg"
2511            if os.path.exists(tname):
2512                fn = tname
2513            else:
2514                vedo.logger.error(f"texture file {tname} does not exist")
2515                return self
2516
2517            fnl = fn.lower()
2518            if ".jpg" in fnl or ".jpeg" in fnl:
2519                reader = vtki.new("JPEGReader")
2520            elif ".png" in fnl:
2521                reader = vtki.new("PNGReader")
2522            elif ".bmp" in fnl:
2523                reader = vtki.new("BMPReader")
2524            else:
2525                vedo.logger.error("in texture() supported files are only PNG, BMP or JPG")
2526                return self
2527            reader.SetFileName(fn)
2528            reader.Update()
2529            out_img = reader.GetOutput()
2530
2531        else:
2532            vedo.logger.error(f"in texture() cannot understand input {type(tname)}")
2533            return self
2534
2535        if tcoords is not None:
2536
2537            if isinstance(tcoords, str):
2538                vtarr = pd.GetPointData().GetArray(tcoords)
2539
2540            else:
2541                tcoords = np.asarray(tcoords)
2542                if tcoords.ndim != 2:
2543                    vedo.logger.error("tcoords must be a 2-dimensional array")
2544                    return self
2545                if tcoords.shape[0] != pd.GetNumberOfPoints():
2546                    vedo.logger.error("nr of texture coords must match nr of points")
2547                    return self
2548                if tcoords.shape[1] != 2:
2549                    vedo.logger.error("tcoords texture vector must have 2 components")
2550                vtarr = utils.numpy2vtk(tcoords)
2551                vtarr.SetName("TCoordinates")
2552
2553            pd.GetPointData().SetTCoords(vtarr)
2554            pd.GetPointData().Modified()
2555
2556        elif not pd.GetPointData().GetTCoords():
2557
2558            # TCoords still void..
2559            # check that there are no texture-like arrays:
2560            names = self.pointdata.keys()
2561            candidate_arr = ""
2562            for name in names:
2563                vtarr = pd.GetPointData().GetArray(name)
2564                if vtarr.GetNumberOfComponents() != 2:
2565                    continue
2566                t0, t1 = vtarr.GetRange()
2567                if t0 >= 0 and t1 <= 1:
2568                    candidate_arr = name
2569
2570            if candidate_arr:
2571
2572                vtarr = pd.GetPointData().GetArray(candidate_arr)
2573                pd.GetPointData().SetTCoords(vtarr)
2574                pd.GetPointData().Modified()
2575
2576            else:
2577                # last resource is automatic mapping
2578                tmapper = vtki.new("TextureMapToPlane")
2579                tmapper.AutomaticPlaneGenerationOn()
2580                tmapper.SetInputData(pd)
2581                tmapper.Update()
2582                tc = tmapper.GetOutput().GetPointData().GetTCoords()
2583                if scale or ushift or vshift:
2584                    ntc = utils.vtk2numpy(tc)
2585                    if scale:
2586                        ntc *= scale
2587                    if ushift:
2588                        ntc[:, 0] += ushift
2589                    if vshift:
2590                        ntc[:, 1] += vshift
2591                    tc = utils.numpy2vtk(tc)
2592                pd.GetPointData().SetTCoords(tc)
2593                pd.GetPointData().Modified()
2594
2595        if out_img:
2596            tu.SetInputData(out_img)
2597        tu.SetInterpolate(interpolate)
2598        tu.SetRepeat(repeat)
2599        tu.SetEdgeClamp(edge_clamp)
2600
2601        self.properties.SetColor(1, 1, 1)
2602        self.mapper.ScalarVisibilityOff()
2603        self.actor.SetTexture(tu)
2604
2605        # if seam_threshold is not None:
2606        #     tname = self.dataset.GetPointData().GetTCoords().GetName()
2607        #     grad = self.gradient(tname)
2608        #     ugrad, vgrad = np.split(grad, 2, axis=1)
2609        #     ugradm, vgradm = utils.mag2(ugrad), utils.mag2(vgrad)
2610        #     gradm = np.log(ugradm + vgradm)
2611        #     largegrad_ids = np.arange(len(grad))[gradm > seam_threshold * 4]
2612        #     uvmap = self.pointdata[tname]
2613        #     # collapse triangles that have large gradient
2614        #     new_points = self.vertices.copy()
2615        #     for f in self.cells:
2616        #         if np.isin(f, largegrad_ids).all():
2617        #             id1, id2, id3 = f
2618        #             uv1, uv2, uv3 = uvmap[f]
2619        #             d12 = utils.mag2(uv1 - uv2)
2620        #             d23 = utils.mag2(uv2 - uv3)
2621        #             d31 = utils.mag2(uv3 - uv1)
2622        #             idm = np.argmin([d12, d23, d31])
2623        #             if idm == 0:
2624        #                 new_points[id1] = new_points[id3]
2625        #                 new_points[id2] = new_points[id3]
2626        #             elif idm == 1:
2627        #                 new_points[id2] = new_points[id1]
2628        #                 new_points[id3] = new_points[id1]
2629        #     self.vertices = new_points
2630
2631        self.dataset.Modified()
2632        return self
2633
2634########################################################################################
2635class VolumeVisual(CommonVisual):
2636    """Class to manage the visual aspects of a ``Volume`` object."""
2637
2638    def __init__(self) -> None:
2639        # print("INIT VolumeVisual")
2640        super().__init__()
2641
2642    def alpha_unit(self, u=None) -> Union[Self, float]:
2643        """
2644        Defines light attenuation per unit length. Default is 1.
2645        The larger the unit length, the further light has to travel to attenuate the same amount.
2646
2647        E.g., if you set the unit distance to 0, you will get full opacity.
2648        It means that when light travels 0 distance it's already attenuated a finite amount.
2649        Thus, any finite distance should attenuate all light.
2650        The larger you make the unit distance, the more transparent the rendering becomes.
2651        """
2652        if u is None:
2653            return self.properties.GetScalarOpacityUnitDistance()
2654        self.properties.SetScalarOpacityUnitDistance(u)
2655        return self
2656
2657    def alpha_gradient(self, alpha_grad, vmin=None, vmax=None) -> Self:
2658        """
2659        Assign a set of tranparencies to a volume's gradient
2660        along the range of the scalar value.
2661        A single constant value can also be assigned.
2662        The gradient function is used to decrease the opacity
2663        in the "flat" regions of the volume while maintaining the opacity
2664        at the boundaries between material types.  The gradient is measured
2665        as the amount by which the intensity changes over unit distance.
2666
2667        The format for alpha_grad is the same as for method `volume.alpha()`.
2668        """
2669        if vmin is None:
2670            vmin, _ = self.dataset.GetScalarRange()
2671        if vmax is None:
2672            _, vmax = self.dataset.GetScalarRange()
2673
2674        if alpha_grad is None:
2675            self.properties.DisableGradientOpacityOn()
2676            return self
2677
2678        self.properties.DisableGradientOpacityOff()
2679
2680        gotf = self.properties.GetGradientOpacity()
2681        if utils.is_sequence(alpha_grad):
2682            alpha_grad = np.array(alpha_grad)
2683            if len(alpha_grad.shape) == 1:  # user passing a flat list e.g. (0.0, 0.3, 0.9, 1)
2684                for i, al in enumerate(alpha_grad):
2685                    xalpha = vmin + (vmax - vmin) * i / (len(alpha_grad) - 1)
2686                    # Create transfer mapping scalar value to gradient opacity
2687                    gotf.AddPoint(xalpha, al)
2688            elif len(alpha_grad.shape) == 2:  # user passing [(x0,alpha0), ...]
2689                gotf.AddPoint(vmin, alpha_grad[0][1])
2690                for xalpha, al in alpha_grad:
2691                    # Create transfer mapping scalar value to opacity
2692                    gotf.AddPoint(xalpha, al)
2693                gotf.AddPoint(vmax, alpha_grad[-1][1])
2694            # print("alpha_grad at", round(xalpha, 1), "\tset to", al)
2695        else:
2696            gotf.AddPoint(vmin, alpha_grad)  # constant alpha_grad
2697            gotf.AddPoint(vmax, alpha_grad)
2698        return self
2699
2700    def cmap(self, c, alpha=None, vmin=None, vmax=None) -> Self:
2701        """Same as `color()`.
2702
2703        Arguments:
2704            alpha : (list)
2705                use a list to specify transparencies along the scalar range
2706            vmin : (float)
2707                force the min of the scalar range to be this value
2708            vmax : (float)
2709                force the max of the scalar range to be this value
2710        """
2711        return self.color(c, alpha, vmin, vmax)
2712
2713    def jittering(self, status=None) -> Union[Self, bool]:
2714        """
2715        If `True`, each ray traversal direction will be perturbed slightly
2716        using a noise-texture to get rid of wood-grain effects.
2717        """
2718        if hasattr(self.mapper, "SetUseJittering"):  # tetmesh doesnt have it
2719            if status is None:
2720                return self.mapper.GetUseJittering()
2721            self.mapper.SetUseJittering(status)
2722        return self
2723
2724    def hide_voxels(self, ids) -> Self:
2725        """
2726        Hide voxels (cells) from visualization.
2727
2728        Example:
2729            ```python
2730            from vedo import *
2731            embryo = Volume(dataurl+'embryo.tif')
2732            embryo.hide_voxels(list(range(400000)))
2733            show(embryo, axes=1).close()
2734            ```
2735
2736        See also:
2737            `volume.mask()`
2738        """
2739        ghost_mask = np.zeros(self.ncells, dtype=np.uint8)
2740        ghost_mask[ids] = vtki.vtkDataSetAttributes.HIDDENCELL
2741        name = vtki.vtkDataSetAttributes.GhostArrayName()
2742        garr = utils.numpy2vtk(ghost_mask, name=name, dtype=np.uint8)
2743        self.dataset.GetCellData().AddArray(garr)
2744        self.dataset.GetCellData().Modified()
2745        return self
2746
2747    def mask(self, data) -> Self:
2748        """
2749        Mask a volume visualization with a binary value.
2750        Needs to specify `volume.mapper = "gpu"`.
2751
2752        Example:
2753        ```python
2754        from vedo import np, Volume, show
2755        data_matrix = np.zeros([75, 75, 75], dtype=np.uint8)
2756        # all voxels have value zero except:
2757        data_matrix[ 0:35,  0:35,  0:35] = 1
2758        data_matrix[35:55, 35:55, 35:55] = 2
2759        data_matrix[55:74, 55:74, 55:74] = 3
2760        vol = Volume(data_matrix).cmap('Blues')
2761        vol.mapper = "gpu"
2762        data_mask = np.zeros_like(data_matrix)
2763        data_mask[10:65, 10:60, 20:70] = 1
2764        vol.mask(data_mask)
2765        show(vol, axes=1).close()
2766        ```
2767        See also:
2768            `volume.hide_voxels()`
2769        """
2770        rdata = data.astype(np.uint8).ravel(order="F")
2771        varr = utils.numpy2vtk(rdata, name="input_mask")
2772
2773        img = vtki.vtkImageData()
2774        img.SetDimensions(self.dimensions())
2775        img.GetPointData().AddArray(varr)
2776        img.GetPointData().SetActiveScalars(varr.GetName())
2777
2778        try:
2779            self.mapper.SetMaskTypeToBinary()
2780            self.mapper.SetMaskInput(img)
2781        except AttributeError:
2782            vedo.logger.error("volume.mask() must specify volume.mapper = 'gpu'")
2783        return self
2784
2785
2786    def mode(self, mode=None) -> Union[Self, int]:
2787        """
2788        Define the volumetric rendering mode following this:
2789            - 0, composite rendering
2790            - 1, maximum projection rendering
2791            - 2, minimum projection rendering
2792            - 3, average projection rendering
2793            - 4, additive mode
2794
2795        The default mode is "composite" where the scalar values are sampled through
2796        the volume and composited in a front-to-back scheme through alpha blending.
2797        The final color and opacity is determined using the color and opacity transfer
2798        functions specified in alpha keyword.
2799
2800        Maximum and minimum intensity blend modes use the maximum and minimum
2801        scalar values, respectively, along the sampling ray.
2802        The final color and opacity is determined by passing the resultant value
2803        through the color and opacity transfer functions.
2804
2805        Additive blend mode accumulates scalar values by passing each value
2806        through the opacity transfer function and then adding up the product
2807        of the value and its opacity. In other words, the scalar values are scaled
2808        using the opacity transfer function and summed to derive the final color.
2809        Note that the resulting image is always grayscale i.e. aggregated values
2810        are not passed through the color transfer function.
2811        This is because the final value is a derived value and not a real data value
2812        along the sampling ray.
2813
2814        Average intensity blend mode works similar to the additive blend mode where
2815        the scalar values are multiplied by opacity calculated from the opacity
2816        transfer function and then added.
2817        The additional step here is to divide the sum by the number of samples
2818        taken through the volume.
2819        As is the case with the additive intensity projection, the final image will
2820        always be grayscale i.e. the aggregated values are not passed through the
2821        color transfer function.
2822        """
2823        if mode is None:
2824            return self.mapper.GetBlendMode()
2825
2826        if isinstance(mode, str):
2827            if "comp" in mode:
2828                mode = 0
2829            elif "proj" in mode:
2830                if "max" in mode:
2831                    mode = 1
2832                elif "min" in mode:
2833                    mode = 2
2834                elif "ave" in mode:
2835                    mode = 3
2836                else:
2837                    vedo.logger.warning(f"unknown mode {mode}")
2838                    mode = 0
2839            elif "add" in mode:
2840                mode = 4
2841            else:
2842                vedo.logger.warning(f"unknown mode {mode}")
2843                mode = 0
2844
2845        self.mapper.SetBlendMode(mode)
2846        return self
2847
2848    def shade(self, status=None) -> Union[Self, bool]:
2849        """
2850        Set/Get the shading of a Volume.
2851        Shading can be further controlled with `volume.lighting()` method.
2852
2853        If shading is turned on, the mapper may perform shading calculations.
2854        In some cases shading does not apply
2855        (for example, in maximum intensity projection mode).
2856        """
2857        if status is None:
2858            return self.properties.GetShade()
2859        self.properties.SetShade(status)
2860        return self
2861
2862    def interpolation(self, itype) -> Self:
2863        """
2864        Set interpolation type.
2865
2866        0=nearest neighbour, 1=linear
2867        """
2868        self.properties.SetInterpolationType(itype)
2869        return self
2870
2871
2872########################################################################################
2873class ImageVisual(CommonVisual, Actor3DHelper):
2874
2875    def __init__(self) -> None:
2876        # print("init ImageVisual")
2877        super().__init__()
2878
2879    def memory_size(self) -> int:
2880        """
2881        Return the size in bytes of the object in memory.
2882        """
2883        return self.dataset.GetActualMemorySize()
2884
2885    def scalar_range(self) -> np.ndarray:
2886        """
2887        Return the scalar range of the image.
2888        """
2889        return np.array(self.dataset.GetScalarRange())
2890
2891    def alpha(self, a=None) -> Union[Self, float]:
2892        """Set/get image's transparency in the rendering scene."""
2893        if a is not None:
2894            self.properties.SetOpacity(a)
2895            return self
2896        return self.properties.GetOpacity()
2897
2898    def level(self, value=None) -> Union[Self, float]:
2899        """Get/Set the image color level (brightness) in the rendering scene."""
2900        if value is None:
2901            return self.properties.GetColorLevel()
2902        self.properties.SetColorLevel(value)
2903        return self
2904
2905    def window(self, value=None) -> Union[Self, float]:
2906        """Get/Set the image color window (contrast) in the rendering scene."""
2907        if value is None:
2908            return self.properties.GetColorWindow()
2909        self.properties.SetColorWindow(value)
2910        return self
2911
2912
2913class LightKit:
2914    """
2915    A LightKit consists of three lights, a 'key light', a 'fill light', and a 'head light'.
2916    
2917    The main light is the key light. It is usually positioned so that it appears like
2918    an overhead light (like the sun, or a ceiling light).
2919    It is generally positioned to shine down on the scene from about a 45 degree angle vertically
2920    and at least a little offset side to side. The key light usually at least about twice as bright
2921    as the total of all other lights in the scene to provide good modeling of object features.
2922
2923    The other lights in the kit (the fill light, headlight, and a pair of back lights)
2924    are weaker sources that provide extra illumination to fill in the spots that the key light misses.
2925    The fill light is usually positioned across from or opposite from the key light
2926    (though still on the same side of the object as the camera) in order to simulate diffuse reflections
2927    from other objects in the scene. 
2928    
2929    The headlight, always located at the position of the camera, reduces the contrast between areas lit
2930    by the key and fill light. The two back lights, one on the left of the object as seen from the observer
2931    and one on the right, fill on the high-contrast areas behind the object.
2932    To enforce the relationship between the different lights, the intensity of the fill, back and headlights
2933    are set as a ratio to the key light brightness.
2934    Thus, the brightness of all the lights in the scene can be changed by changing the key light intensity.
2935
2936    All lights are directional lights, infinitely far away with no falloff. Lights move with the camera.
2937
2938    For simplicity, the position of lights in the LightKit can only be specified using angles:
2939    the elevation (latitude) and azimuth (longitude) of each light with respect to the camera, in degrees.
2940    For example, a light at (elevation=0, azimuth=0) is located at the camera (a headlight).
2941    A light at (elevation=90, azimuth=0) is above the lookat point, shining down.
2942    Negative azimuth values move the lights clockwise as seen above, positive values counter-clockwise.
2943    So, a light at (elevation=45, azimuth=-20) is above and in front of the object and shining
2944    slightly from the left side.
2945
2946    LightKit limits the colors that can be assigned to any light to those of incandescent sources such as
2947    light bulbs and sunlight. It defines a special color spectrum called "warmth" from which light colors
2948    can be chosen, where 0 is cold blue, 0.5 is neutral white, and 1 is deep sunset red.
2949    Colors close to 0.5 are "cool whites" and "warm whites," respectively.
2950
2951    Since colors far from white on the warmth scale appear less bright, key-to-fill and key-to-headlight
2952    ratios are skewed by key, fill, and headlight colors. If `maintain_luminance` is set, LightKit will
2953    attempt to compensate for these perceptual differences by increasing the brightness of more saturated colors.
2954
2955    To specify the color of a light, positioning etc you can pass a dictionary with the following keys:
2956        - `intensity` : (float) The intensity of the key light. Default is 1.
2957        - `ratio`     : (float) The ratio of the light intensity wrt key light.
2958        - `warmth`    : (float) The warmth of the light. Default is 0.5.
2959        - `elevation` : (float) The elevation of the light in degrees.
2960        - `azimuth`   : (float) The azimuth of the light in degrees.
2961
2962    Example:
2963        ```python
2964        from vedo import *
2965        lightkit = LightKit(head={"warmth":0.6})
2966        mesh = Mesh(dataurl+"bunny.obj")
2967        plt = Plotter()
2968        plt.remove_lights().add(mesh, lightkit)
2969        plt.show().close()
2970        ```
2971    """
2972    def __init__(self, key=(), fill=(), back=(), head=(), maintain_luminance=False) -> None:
2973
2974        self.lightkit = vtki.new("LightKit")
2975        self.lightkit.SetMaintainLuminance(maintain_luminance)
2976        self.key  = dict(key)
2977        self.head = dict(head)
2978        self.fill = dict(fill)
2979        self.back = dict(back)
2980        self.update()
2981
2982    def update(self) -> None:
2983        """Update the LightKit properties."""
2984        if "warmth" in self.key:
2985            self.lightkit.SetKeyLightWarmth(self.key["warmth"])
2986        if "warmth" in self.fill:
2987            self.lightkit.SetFillLightWarmth(self.fill["warmth"])
2988        if "warmth" in self.head:
2989            self.lightkit.SetHeadLightWarmth(self.head["warmth"])
2990        if "warmth" in self.back:
2991            self.lightkit.SetBackLightWarmth(self.back["warmth"])
2992
2993        if "intensity" in self.key:
2994            self.lightkit.SetKeyLightIntensity(self.key["intensity"])
2995        if "ratio" in self.fill:
2996            self.lightkit.SetKeyToFillRatio(self.key["ratio"])
2997        if "ratio" in self.head:
2998            self.lightkit.SetKeyToHeadRatio(self.key["ratio"])
2999        if "ratio" in self.back:
3000            self.lightkit.SetKeyToBackRatio(self.key["ratio"])
3001
3002        if "elevation" in self.key:
3003            self.lightkit.SetKeyLightElevation(self.key["elevation"])
3004        if "elevation" in self.fill:
3005            self.lightkit.SetFillLightElevation(self.fill["elevation"])
3006        if "elevation" in self.head:
3007            self.lightkit.SetHeadLightElevation(self.head["elevation"])
3008        if "elevation" in self.back:
3009            self.lightkit.SetBackLightElevation(self.back["elevation"])
3010
3011        if "azimuth" in self.key:
3012            self.lightkit.SetKeyLightAzimuth(self.key["azimuth"])
3013        if "azimuth" in self.fill:
3014            self.lightkit.SetFillLightAzimuth(self.fill["azimuth"])
3015        if "azimuth" in self.head:
3016            self.lightkit.SetHeadLightAzimuth(self.head["azimuth"])
3017        if "azimuth" in self.back:
3018            self.lightkit.SetBackLightAzimuth(self.back["azimuth"])
class CommonVisual:
 35class CommonVisual:
 36    """Class to manage the visual aspects common to all objects."""
 37
 38    def __init__(self):
 39        # print("init CommonVisual", type(self))
 40
 41        self.properties = None
 42
 43        self.scalarbar = None       
 44        self.pipeline = None
 45
 46        self.trail = None
 47        self.trail_points = []
 48        self.trail_segment_size = 0
 49        self.trail_offset = None
 50
 51        self.shadows = []
 52
 53        self.axes = None
 54        self.picked3d = None
 55
 56        self.rendered_at = set()
 57
 58        self._ligthingnr = 0  # index of the lighting mode changed from CLI
 59        self._cmap_name = ""  # remember the cmap name for self._keypress
 60        self._caption = None
 61
 62
 63    def print(self):
 64        """Print object info."""
 65        print(self.__str__())
 66        return self
 67
 68    @property
 69    def LUT(self) -> np.ndarray:
 70        """Return the lookup table of the object as a numpy object."""
 71        try:
 72            _lut = self.mapper.GetLookupTable()
 73            values = []
 74            for i in range(_lut.GetTable().GetNumberOfTuples()):
 75                # print("LUT i =", i, "value =", _lut.GetTableValue(i))
 76                values.append(_lut.GetTableValue(i))
 77            return np.array(values)
 78        except AttributeError:
 79            return np.array([], dtype=float)
 80        
 81    @LUT.setter
 82    def LUT(self, arr):
 83        """
 84        Set the lookup table of the object from a numpy or `vtkLookupTable` object.
 85        Consider using `cmap()` or `build_lut()` instead as it allows
 86        to set the range of the LUT and to use a string name for the color map.
 87        """
 88        if isinstance(arr, vtki.vtkLookupTable):
 89            newlut = arr
 90            self.mapper.SetScalarRange(newlut.GetRange())
 91        else:
 92            newlut = vtki.vtkLookupTable()
 93            newlut.SetNumberOfTableValues(len(arr))
 94            if len(arr[0]) == 3:
 95                arr = np.insert(arr, 3, 1, axis=1)
 96            for i, v in enumerate(arr):
 97                newlut.SetTableValue(i, v)
 98            newlut.SetRange(self.mapper.GetScalarRange())
 99            # print("newlut.GetRange() =", newlut.GetRange())
100            # print("self.mapper.GetScalarRange() =", self.mapper.GetScalarRange())
101            newlut.Build()
102        self.mapper.SetLookupTable(newlut)
103        self.mapper.ScalarVisibilityOn()
104    
105    def scalar_range(self, vmin=None, vmax=None):
106        """Set the range of the scalar value for visualization."""
107        if vmin is None and vmax is None:
108            return np.array(self.mapper.GetScalarRange())
109        if vmax is None:
110            vmin, vmax = vmin # assume it is a list
111        self.mapper.SetScalarRange(float(vmin), float(vmax))
112        return self
113
114    def add_observer(self, event_name, func, priority=0) -> int:
115        """Add a callback function that will be called when an event occurs."""
116        event_name = utils.get_vtk_name_event(event_name)
117        idd = self.actor.AddObserver(event_name, func, priority)
118        return idd
119    
120    def invoke_event(self, event_name) -> Self:
121        """Invoke an event."""
122        event_name = utils.get_vtk_name_event(event_name)
123        self.actor.InvokeEvent(event_name)
124        return self
125    
126    # def abort_event(self, obs_id):
127    #     """Abort an event."""
128    #     cmd = self.actor.GetCommand(obs_id) # vtkCommand
129    #     if cmd:
130    #         cmd.AbortFlagOn()
131    #     return self
132
133    def show(self, **options) -> Union["vedo.Plotter", None]:
134        """
135        Create on the fly an instance of class `Plotter` or use the last existing one to
136        show one single object.
137
138        This method is meant as a shortcut. If more than one object needs to be visualised
139        please use the syntax `show(mesh1, mesh2, volume, ..., options)`.
140
141        Returns the `Plotter` class instance.
142        """
143        return vedo.plotter.show(self, **options)
144
145    def thumbnail(self, zoom=1.25, size=(200, 200), bg="white", azimuth=0, elevation=0, axes=False) -> np.ndarray:
146        """Build a thumbnail of the object and return it as an array."""
147        # speed is about 20Hz for size=[200,200]
148        ren = vtki.vtkRenderer()
149
150        actor = self.actor
151        if isinstance(self, vedo.UnstructuredGrid):
152            geo = vtki.new("GeometryFilter")
153            geo.SetInputData(self.dataset)
154            geo.Update()
155            actor = vedo.Mesh(geo.GetOutput()).cmap("rainbow").actor
156
157        ren.AddActor(actor)
158        if axes:
159            axes = vedo.addons.Axes(self)
160            ren.AddActor(axes.actor)
161        ren.ResetCamera()
162        cam = ren.GetActiveCamera()
163        cam.Zoom(zoom)
164        cam.Elevation(elevation)
165        cam.Azimuth(azimuth)
166
167        ren_win = vtki.vtkRenderWindow()
168        ren_win.SetOffScreenRendering(True)
169        ren_win.SetSize(size)
170        ren.SetBackground(colors.get_color(bg))
171        ren_win.AddRenderer(ren)
172        ren.ResetCameraClippingRange()
173        ren_win.Render()
174
175        nx, ny = ren_win.GetSize()
176        arr = vtki.vtkUnsignedCharArray()
177        ren_win.GetRGBACharPixelData(0, 0, nx - 1, ny - 1, 0, arr)
178        narr = utils.vtk2numpy(arr).T[:3].T.reshape([ny, nx, 3])
179        narr = np.ascontiguousarray(np.flip(narr, axis=0))
180
181        ren.RemoveActor(actor)
182        if axes:
183            ren.RemoveActor(axes.actor)
184        ren_win.Finalize()
185        del ren_win
186        return narr
187
188    def pickable(self, value=None) -> Self:
189        """Set/get the pickability property of an object."""
190        if value is None:
191            return self.actor.GetPickable()
192        self.actor.SetPickable(value)
193        return self
194
195    def use_bounds(self, value=True) -> Self:
196        """
197        Instruct the current camera to either take into account or ignore
198        the object bounds when resetting.
199        """
200        self.actor.SetUseBounds(value)
201        return self
202
203    def draggable(self, value=None) -> Self:  # NOT FUNCTIONAL?
204        """Set/get the draggability property of an object."""
205        if value is None:
206            return self.actor.GetDragable()
207        self.actor.SetDragable(value)
208        return self
209
210    def on(self) -> Self:
211        """Switch on  object visibility. Object is not removed."""
212        self.actor.VisibilityOn()
213        try:
214            self.scalarbar.actor.VisibilityOn()
215        except AttributeError:
216            pass
217        try:
218            self.trail.actor.VisibilityOn()
219        except AttributeError:
220            pass
221        try:
222            for sh in self.shadows:
223                sh.actor.VisibilityOn()
224        except AttributeError:
225            pass
226        return self
227
228    def off(self) -> Self:
229        """Switch off object visibility. Object is not removed."""
230        self.actor.VisibilityOff()
231        try:
232            self.scalarbar.actor.VisibilityOff()
233        except AttributeError:
234            pass
235        try:
236            self.trail.actor.VisibilityOff()
237        except AttributeError:
238            pass
239        try:
240            for sh in self.shadows:
241                sh.actor.VisibilityOff()
242        except AttributeError:
243            pass
244        return self
245
246    def toggle(self) -> Self:
247        """Toggle object visibility on/off."""
248        v = self.actor.GetVisibility()
249        if v:
250            self.off()
251        else:
252            self.on()
253        return self
254
255    def add_scalarbar(
256        self,
257        title="",
258        pos=(),
259        size=(80, 400),
260        font_size=14,
261        title_yoffset=15,
262        nlabels=None,
263        c=None,
264        horizontal=False,
265        use_alpha=True,
266        label_format=":6.3g",
267    ) -> Self:
268        """
269        Add a 2D scalar bar for the specified object.
270
271        Arguments:
272            title : (str)
273                scalar bar title
274            pos : (list)
275                position coordinates of the bottom left corner.
276                Can also be a pair of (x,y) values in the range [0,1]
277                to indicate the position of the bottom left and top right corners.
278            size : (float,float)
279                size of the scalarbar in number of pixels (width, height)
280            font_size : (float)
281                size of font for title and numeric labels
282            title_yoffset : (float)
283                vertical space offset between title and color scalarbar
284            nlabels : (int)
285                number of numeric labels
286            c : (list)
287                color of the scalar bar text
288            horizontal : (bool)
289                lay the scalarbar horizontally
290            use_alpha : (bool)
291                render transparency in the color bar itself
292            label_format : (str)
293                c-style format string for numeric labels
294
295        Examples:
296            - [mesh_coloring.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mesh_coloring.py)
297            - [scalarbars.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/scalarbars.py)
298        """
299        plt = vedo.plotter_instance
300
301        if plt and plt.renderer:
302            c = (0.9, 0.9, 0.9)
303            if np.sum(plt.renderer.GetBackground()) > 1.5:
304                c = (0.1, 0.1, 0.1)
305            if isinstance(self.scalarbar, vtki.vtkActor):
306                plt.renderer.RemoveActor(self.scalarbar)
307            elif isinstance(self.scalarbar, vedo.Assembly):
308                for a in self.scalarbar.unpack():
309                    plt.renderer.RemoveActor(a)
310        if c is None:
311            c = "gray"
312
313        sb = vedo.addons.ScalarBar(
314            self,
315            title,
316            pos,
317            size,
318            font_size,
319            title_yoffset,
320            nlabels,
321            c,
322            horizontal,
323            use_alpha,
324            label_format,
325        )
326        self.scalarbar = sb
327        return self
328
329    def add_scalarbar3d(
330        self,
331        title="",
332        pos=None,
333        size=(0, 0),
334        title_font="",
335        title_xoffset=-1.2,
336        title_yoffset=0.0,
337        title_size=1.5,
338        title_rotation=0.0,
339        nlabels=9,
340        label_font="",
341        label_size=1,
342        label_offset=0.375,
343        label_rotation=0,
344        label_format="",
345        italic=0,
346        c=None,
347        draw_box=True,
348        above_text=None,
349        below_text=None,
350        nan_text="NaN",
351        categories=None,
352    ) -> Self:
353        """
354        Associate a 3D scalar bar to the object and add it to the scene.
355        The new scalarbar object (Assembly) will be accessible as obj.scalarbar
356
357        Arguments:
358            size : (list)
359                (thickness, length) of scalarbar
360            title : (str)
361                scalar bar title
362            title_xoffset : (float)
363                horizontal space btw title and color scalarbar
364            title_yoffset : (float)
365                vertical space offset
366            title_size : (float)
367                size of title wrt numeric labels
368            title_rotation : (float)
369                title rotation in degrees
370            nlabels : (int)
371                number of numeric labels
372            label_font : (str)
373                font type for labels
374            label_size : (float)
375                label scale factor
376            label_offset : (float)
377                space btw numeric labels and scale
378            label_rotation : (float)
379                label rotation in degrees
380            label_format : (str)
381                label format for floats and integers (e.g. `':.2f'`)
382            draw_box : (bool)
383                draw a box around the colorbar
384            categories : (list)
385                make a categorical scalarbar,
386                the input list will have the format `[value, color, alpha, textlabel]`
387
388        Examples:
389            - [scalarbars.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/scalarbars.py)
390        """
391        plt = vedo.plotter_instance
392        if plt and c is None:  # automatic black or white
393            c = (0.9, 0.9, 0.9)
394            if np.sum(vedo.get_color(plt.backgrcol)) > 1.5:
395                c = (0.1, 0.1, 0.1)
396        if c is None:
397            c = (0, 0, 0)
398        c = vedo.get_color(c)
399
400        self.scalarbar = vedo.addons.ScalarBar3D(
401            self,
402            title,
403            pos,
404            size,
405            title_font,
406            title_xoffset,
407            title_yoffset,
408            title_size,
409            title_rotation,
410            nlabels,
411            label_font,
412            label_size,
413            label_offset,
414            label_rotation,
415            label_format,
416            italic,
417            c,
418            draw_box,
419            above_text,
420            below_text,
421            nan_text,
422            categories,
423        )
424        return self
425
426    def color(self, col, alpha=None, vmin=None, vmax=None):
427        """
428        Assign a color or a set of colors along the range of the scalar value.
429        A single constant color can also be assigned.
430        Any matplotlib color map name is also accepted, e.g. `volume.color('jet')`.
431
432        E.g.: say that your cells scalar runs from -3 to 6,
433        and you want -3 to show red and 1.5 violet and 6 green, then just set:
434
435        `volume.color(['red', 'violet', 'green'])`
436
437        You can also assign a specific color to a aspecific value with eg.:
438
439        `volume.color([(0,'red'), (0.5,'violet'), (1,'green')])`
440
441        Arguments:
442            alpha : (list)
443                use a list to specify transparencies along the scalar range
444            vmin : (float)
445                force the min of the scalar range to be this value
446            vmax : (float)
447                force the max of the scalar range to be this value
448        """
449        # supersedes method in Points, Mesh
450
451        if col is None:
452            return self
453
454        if vmin is None:
455            vmin, _ = self.dataset.GetScalarRange()
456        if vmax is None:
457            _, vmax = self.dataset.GetScalarRange()
458        ctf = self.properties.GetRGBTransferFunction()
459        ctf.RemoveAllPoints()
460
461        if utils.is_sequence(col):
462            if utils.is_sequence(col[0]) and len(col[0]) == 2:
463                # user passing [(value1, color1), ...]
464                for x, ci in col:
465                    r, g, b = colors.get_color(ci)
466                    ctf.AddRGBPoint(x, r, g, b)
467                    # colors.printc('color at', round(x, 1),
468                    #               'set to', colors.get_color_name((r, g, b)), bold=0)
469            else:
470                # user passing [color1, color2, ..]
471                for i, ci in enumerate(col):
472                    r, g, b = colors.get_color(ci)
473                    x = vmin + (vmax - vmin) * i / (len(col) - 1)
474                    ctf.AddRGBPoint(x, r, g, b)
475        elif isinstance(col, str):
476            if col in colors.colors.keys() or col in colors.color_nicks.keys():
477                r, g, b = colors.get_color(col)
478                ctf.AddRGBPoint(vmin, r, g, b)  # constant color
479                ctf.AddRGBPoint(vmax, r, g, b)
480            else:  # assume it's a colormap
481                for x in np.linspace(vmin, vmax, num=64, endpoint=True):
482                    r, g, b = colors.color_map(x, name=col, vmin=vmin, vmax=vmax)
483                    ctf.AddRGBPoint(x, r, g, b)
484        elif isinstance(col, int):
485            r, g, b = colors.get_color(col)
486            ctf.AddRGBPoint(vmin, r, g, b)  # constant color
487            ctf.AddRGBPoint(vmax, r, g, b)
488        else:
489            vedo.logger.warning(f"in color() unknown input type {type(col)}")
490
491        if alpha is not None:
492            self.alpha(alpha, vmin=vmin, vmax=vmax)
493        return self
494
495    def alpha(self, alpha, vmin=None, vmax=None) -> Self:
496        """
497        Assign a set of tranparencies along the range of the scalar value.
498        A single constant value can also be assigned.
499
500        E.g.: say `alpha=(0.0, 0.3, 0.9, 1)` and the scalar range goes from -10 to 150.
501        Then all cells with a value close to -10 will be completely transparent, cells at 1/4
502        of the range will get an alpha equal to 0.3 and voxels with value close to 150
503        will be completely opaque.
504
505        As a second option one can set explicit (x, alpha_x) pairs to define the transfer function.
506
507        E.g.: say `alpha=[(-5, 0), (35, 0.4) (123,0.9)]` and the scalar range goes from -10 to 150.
508        Then all cells below -5 will be completely transparent, cells with a scalar value of 35
509        will get an opacity of 40% and above 123 alpha is set to 90%.
510        """
511        if vmin is None:
512            vmin, _ = self.dataset.GetScalarRange()
513        if vmax is None:
514            _, vmax = self.dataset.GetScalarRange()
515        otf = self.properties.GetScalarOpacity()
516        otf.RemoveAllPoints()
517
518        if utils.is_sequence(alpha):
519            alpha = np.array(alpha)
520            if len(alpha.shape) == 1:  # user passing a flat list e.g. (0.0, 0.3, 0.9, 1)
521                for i, al in enumerate(alpha):
522                    xalpha = vmin + (vmax - vmin) * i / (len(alpha) - 1)
523                    # Create transfer mapping scalar value to opacity
524                    otf.AddPoint(xalpha, al)
525                    # print("alpha at", round(xalpha, 1), "\tset to", al)
526            elif len(alpha.shape) == 2:  # user passing [(x0,alpha0), ...]
527                otf.AddPoint(vmin, alpha[0][1])
528                for xalpha, al in alpha:
529                    # Create transfer mapping scalar value to opacity
530                    otf.AddPoint(xalpha, al)
531                otf.AddPoint(vmax, alpha[-1][1])
532
533        else:
534
535            otf.AddPoint(vmin, alpha)  # constant alpha
536            otf.AddPoint(vmax, alpha)
537
538        return self

Class to manage the visual aspects common to all objects.

CommonVisual()
38    def __init__(self):
39        # print("init CommonVisual", type(self))
40
41        self.properties = None
42
43        self.scalarbar = None       
44        self.pipeline = None
45
46        self.trail = None
47        self.trail_points = []
48        self.trail_segment_size = 0
49        self.trail_offset = None
50
51        self.shadows = []
52
53        self.axes = None
54        self.picked3d = None
55
56        self.rendered_at = set()
57
58        self._ligthingnr = 0  # index of the lighting mode changed from CLI
59        self._cmap_name = ""  # remember the cmap name for self._keypress
60        self._caption = None
def print(self):
63    def print(self):
64        """Print object info."""
65        print(self.__str__())
66        return self

Print object info.

LUT: numpy.ndarray
68    @property
69    def LUT(self) -> np.ndarray:
70        """Return the lookup table of the object as a numpy object."""
71        try:
72            _lut = self.mapper.GetLookupTable()
73            values = []
74            for i in range(_lut.GetTable().GetNumberOfTuples()):
75                # print("LUT i =", i, "value =", _lut.GetTableValue(i))
76                values.append(_lut.GetTableValue(i))
77            return np.array(values)
78        except AttributeError:
79            return np.array([], dtype=float)

Return the lookup table of the object as a numpy object.

def scalar_range(self, vmin=None, vmax=None):
105    def scalar_range(self, vmin=None, vmax=None):
106        """Set the range of the scalar value for visualization."""
107        if vmin is None and vmax is None:
108            return np.array(self.mapper.GetScalarRange())
109        if vmax is None:
110            vmin, vmax = vmin # assume it is a list
111        self.mapper.SetScalarRange(float(vmin), float(vmax))
112        return self

Set the range of the scalar value for visualization.

def add_observer(self, event_name, func, priority=0) -> int:
114    def add_observer(self, event_name, func, priority=0) -> int:
115        """Add a callback function that will be called when an event occurs."""
116        event_name = utils.get_vtk_name_event(event_name)
117        idd = self.actor.AddObserver(event_name, func, priority)
118        return idd

Add a callback function that will be called when an event occurs.

def invoke_event(self, event_name) -> Self:
120    def invoke_event(self, event_name) -> Self:
121        """Invoke an event."""
122        event_name = utils.get_vtk_name_event(event_name)
123        self.actor.InvokeEvent(event_name)
124        return self

Invoke an event.

def show(self, **options) -> Optional[vedo.plotter.Plotter]:
133    def show(self, **options) -> Union["vedo.Plotter", None]:
134        """
135        Create on the fly an instance of class `Plotter` or use the last existing one to
136        show one single object.
137
138        This method is meant as a shortcut. If more than one object needs to be visualised
139        please use the syntax `show(mesh1, mesh2, volume, ..., options)`.
140
141        Returns the `Plotter` class instance.
142        """
143        return vedo.plotter.show(self, **options)

Create on the fly an instance of class Plotter or use the last existing one to show one single object.

This method is meant as a shortcut. If more than one object needs to be visualised please use the syntax show(mesh1, mesh2, volume, ..., options).

Returns the Plotter class instance.

def thumbnail( self, zoom=1.25, size=(200, 200), bg='white', azimuth=0, elevation=0, axes=False) -> numpy.ndarray:
145    def thumbnail(self, zoom=1.25, size=(200, 200), bg="white", azimuth=0, elevation=0, axes=False) -> np.ndarray:
146        """Build a thumbnail of the object and return it as an array."""
147        # speed is about 20Hz for size=[200,200]
148        ren = vtki.vtkRenderer()
149
150        actor = self.actor
151        if isinstance(self, vedo.UnstructuredGrid):
152            geo = vtki.new("GeometryFilter")
153            geo.SetInputData(self.dataset)
154            geo.Update()
155            actor = vedo.Mesh(geo.GetOutput()).cmap("rainbow").actor
156
157        ren.AddActor(actor)
158        if axes:
159            axes = vedo.addons.Axes(self)
160            ren.AddActor(axes.actor)
161        ren.ResetCamera()
162        cam = ren.GetActiveCamera()
163        cam.Zoom(zoom)
164        cam.Elevation(elevation)
165        cam.Azimuth(azimuth)
166
167        ren_win = vtki.vtkRenderWindow()
168        ren_win.SetOffScreenRendering(True)
169        ren_win.SetSize(size)
170        ren.SetBackground(colors.get_color(bg))
171        ren_win.AddRenderer(ren)
172        ren.ResetCameraClippingRange()
173        ren_win.Render()
174
175        nx, ny = ren_win.GetSize()
176        arr = vtki.vtkUnsignedCharArray()
177        ren_win.GetRGBACharPixelData(0, 0, nx - 1, ny - 1, 0, arr)
178        narr = utils.vtk2numpy(arr).T[:3].T.reshape([ny, nx, 3])
179        narr = np.ascontiguousarray(np.flip(narr, axis=0))
180
181        ren.RemoveActor(actor)
182        if axes:
183            ren.RemoveActor(axes.actor)
184        ren_win.Finalize()
185        del ren_win
186        return narr

Build a thumbnail of the object and return it as an array.

def pickable(self, value=None) -> Self:
188    def pickable(self, value=None) -> Self:
189        """Set/get the pickability property of an object."""
190        if value is None:
191            return self.actor.GetPickable()
192        self.actor.SetPickable(value)
193        return self

Set/get the pickability property of an object.

def use_bounds(self, value=True) -> Self:
195    def use_bounds(self, value=True) -> Self:
196        """
197        Instruct the current camera to either take into account or ignore
198        the object bounds when resetting.
199        """
200        self.actor.SetUseBounds(value)
201        return self

Instruct the current camera to either take into account or ignore the object bounds when resetting.

def draggable(self, value=None) -> Self:
203    def draggable(self, value=None) -> Self:  # NOT FUNCTIONAL?
204        """Set/get the draggability property of an object."""
205        if value is None:
206            return self.actor.GetDragable()
207        self.actor.SetDragable(value)
208        return self

Set/get the draggability property of an object.

def on(self) -> Self:
210    def on(self) -> Self:
211        """Switch on  object visibility. Object is not removed."""
212        self.actor.VisibilityOn()
213        try:
214            self.scalarbar.actor.VisibilityOn()
215        except AttributeError:
216            pass
217        try:
218            self.trail.actor.VisibilityOn()
219        except AttributeError:
220            pass
221        try:
222            for sh in self.shadows:
223                sh.actor.VisibilityOn()
224        except AttributeError:
225            pass
226        return self

Switch on object visibility. Object is not removed.

def off(self) -> Self:
228    def off(self) -> Self:
229        """Switch off object visibility. Object is not removed."""
230        self.actor.VisibilityOff()
231        try:
232            self.scalarbar.actor.VisibilityOff()
233        except AttributeError:
234            pass
235        try:
236            self.trail.actor.VisibilityOff()
237        except AttributeError:
238            pass
239        try:
240            for sh in self.shadows:
241                sh.actor.VisibilityOff()
242        except AttributeError:
243            pass
244        return self

Switch off object visibility. Object is not removed.

def toggle(self) -> Self:
246    def toggle(self) -> Self:
247        """Toggle object visibility on/off."""
248        v = self.actor.GetVisibility()
249        if v:
250            self.off()
251        else:
252            self.on()
253        return self

Toggle object visibility on/off.

def add_scalarbar( self, title='', pos=(), size=(80, 400), font_size=14, title_yoffset=15, nlabels=None, c=None, horizontal=False, use_alpha=True, label_format=':6.3g') -> Self:
255    def add_scalarbar(
256        self,
257        title="",
258        pos=(),
259        size=(80, 400),
260        font_size=14,
261        title_yoffset=15,
262        nlabels=None,
263        c=None,
264        horizontal=False,
265        use_alpha=True,
266        label_format=":6.3g",
267    ) -> Self:
268        """
269        Add a 2D scalar bar for the specified object.
270
271        Arguments:
272            title : (str)
273                scalar bar title
274            pos : (list)
275                position coordinates of the bottom left corner.
276                Can also be a pair of (x,y) values in the range [0,1]
277                to indicate the position of the bottom left and top right corners.
278            size : (float,float)
279                size of the scalarbar in number of pixels (width, height)
280            font_size : (float)
281                size of font for title and numeric labels
282            title_yoffset : (float)
283                vertical space offset between title and color scalarbar
284            nlabels : (int)
285                number of numeric labels
286            c : (list)
287                color of the scalar bar text
288            horizontal : (bool)
289                lay the scalarbar horizontally
290            use_alpha : (bool)
291                render transparency in the color bar itself
292            label_format : (str)
293                c-style format string for numeric labels
294
295        Examples:
296            - [mesh_coloring.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mesh_coloring.py)
297            - [scalarbars.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/scalarbars.py)
298        """
299        plt = vedo.plotter_instance
300
301        if plt and plt.renderer:
302            c = (0.9, 0.9, 0.9)
303            if np.sum(plt.renderer.GetBackground()) > 1.5:
304                c = (0.1, 0.1, 0.1)
305            if isinstance(self.scalarbar, vtki.vtkActor):
306                plt.renderer.RemoveActor(self.scalarbar)
307            elif isinstance(self.scalarbar, vedo.Assembly):
308                for a in self.scalarbar.unpack():
309                    plt.renderer.RemoveActor(a)
310        if c is None:
311            c = "gray"
312
313        sb = vedo.addons.ScalarBar(
314            self,
315            title,
316            pos,
317            size,
318            font_size,
319            title_yoffset,
320            nlabels,
321            c,
322            horizontal,
323            use_alpha,
324            label_format,
325        )
326        self.scalarbar = sb
327        return self

Add a 2D scalar bar for the specified object.

Arguments:
  • title : (str) scalar bar title
  • pos : (list) position coordinates of the bottom left corner. Can also be a pair of (x,y) values in the range [0,1] to indicate the position of the bottom left and top right corners.
  • size : (float,float) size of the scalarbar in number of pixels (width, height)
  • font_size : (float) size of font for title and numeric labels
  • title_yoffset : (float) vertical space offset between title and color scalarbar
  • nlabels : (int) number of numeric labels
  • c : (list) color of the scalar bar text
  • horizontal : (bool) lay the scalarbar horizontally
  • use_alpha : (bool) render transparency in the color bar itself
  • label_format : (str) c-style format string for numeric labels
Examples:
def add_scalarbar3d( self, title='', pos=None, size=(0, 0), title_font='', title_xoffset=-1.2, title_yoffset=0.0, title_size=1.5, title_rotation=0.0, nlabels=9, label_font='', label_size=1, label_offset=0.375, label_rotation=0, label_format='', italic=0, c=None, draw_box=True, above_text=None, below_text=None, nan_text='NaN', categories=None) -> Self:
329    def add_scalarbar3d(
330        self,
331        title="",
332        pos=None,
333        size=(0, 0),
334        title_font="",
335        title_xoffset=-1.2,
336        title_yoffset=0.0,
337        title_size=1.5,
338        title_rotation=0.0,
339        nlabels=9,
340        label_font="",
341        label_size=1,
342        label_offset=0.375,
343        label_rotation=0,
344        label_format="",
345        italic=0,
346        c=None,
347        draw_box=True,
348        above_text=None,
349        below_text=None,
350        nan_text="NaN",
351        categories=None,
352    ) -> Self:
353        """
354        Associate a 3D scalar bar to the object and add it to the scene.
355        The new scalarbar object (Assembly) will be accessible as obj.scalarbar
356
357        Arguments:
358            size : (list)
359                (thickness, length) of scalarbar
360            title : (str)
361                scalar bar title
362            title_xoffset : (float)
363                horizontal space btw title and color scalarbar
364            title_yoffset : (float)
365                vertical space offset
366            title_size : (float)
367                size of title wrt numeric labels
368            title_rotation : (float)
369                title rotation in degrees
370            nlabels : (int)
371                number of numeric labels
372            label_font : (str)
373                font type for labels
374            label_size : (float)
375                label scale factor
376            label_offset : (float)
377                space btw numeric labels and scale
378            label_rotation : (float)
379                label rotation in degrees
380            label_format : (str)
381                label format for floats and integers (e.g. `':.2f'`)
382            draw_box : (bool)
383                draw a box around the colorbar
384            categories : (list)
385                make a categorical scalarbar,
386                the input list will have the format `[value, color, alpha, textlabel]`
387
388        Examples:
389            - [scalarbars.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/scalarbars.py)
390        """
391        plt = vedo.plotter_instance
392        if plt and c is None:  # automatic black or white
393            c = (0.9, 0.9, 0.9)
394            if np.sum(vedo.get_color(plt.backgrcol)) > 1.5:
395                c = (0.1, 0.1, 0.1)
396        if c is None:
397            c = (0, 0, 0)
398        c = vedo.get_color(c)
399
400        self.scalarbar = vedo.addons.ScalarBar3D(
401            self,
402            title,
403            pos,
404            size,
405            title_font,
406            title_xoffset,
407            title_yoffset,
408            title_size,
409            title_rotation,
410            nlabels,
411            label_font,
412            label_size,
413            label_offset,
414            label_rotation,
415            label_format,
416            italic,
417            c,
418            draw_box,
419            above_text,
420            below_text,
421            nan_text,
422            categories,
423        )
424        return self

Associate a 3D scalar bar to the object and add it to the scene. The new scalarbar object (Assembly) will be accessible as obj.scalarbar

Arguments:
  • size : (list) (thickness, length) of scalarbar
  • title : (str) scalar bar title
  • title_xoffset : (float) horizontal space btw title and color scalarbar
  • title_yoffset : (float) vertical space offset
  • title_size : (float) size of title wrt numeric labels
  • title_rotation : (float) title rotation in degrees
  • nlabels : (int) number of numeric labels
  • label_font : (str) font type for labels
  • label_size : (float) label scale factor
  • label_offset : (float) space btw numeric labels and scale
  • label_rotation : (float) label rotation in degrees
  • label_format : (str) label format for floats and integers (e.g. ':.2f')
  • draw_box : (bool) draw a box around the colorbar
  • categories : (list) make a categorical scalarbar, the input list will have the format [value, color, alpha, textlabel]
Examples:
def color(self, col, alpha=None, vmin=None, vmax=None):
426    def color(self, col, alpha=None, vmin=None, vmax=None):
427        """
428        Assign a color or a set of colors along the range of the scalar value.
429        A single constant color can also be assigned.
430        Any matplotlib color map name is also accepted, e.g. `volume.color('jet')`.
431
432        E.g.: say that your cells scalar runs from -3 to 6,
433        and you want -3 to show red and 1.5 violet and 6 green, then just set:
434
435        `volume.color(['red', 'violet', 'green'])`
436
437        You can also assign a specific color to a aspecific value with eg.:
438
439        `volume.color([(0,'red'), (0.5,'violet'), (1,'green')])`
440
441        Arguments:
442            alpha : (list)
443                use a list to specify transparencies along the scalar range
444            vmin : (float)
445                force the min of the scalar range to be this value
446            vmax : (float)
447                force the max of the scalar range to be this value
448        """
449        # supersedes method in Points, Mesh
450
451        if col is None:
452            return self
453
454        if vmin is None:
455            vmin, _ = self.dataset.GetScalarRange()
456        if vmax is None:
457            _, vmax = self.dataset.GetScalarRange()
458        ctf = self.properties.GetRGBTransferFunction()
459        ctf.RemoveAllPoints()
460
461        if utils.is_sequence(col):
462            if utils.is_sequence(col[0]) and len(col[0]) == 2:
463                # user passing [(value1, color1), ...]
464                for x, ci in col:
465                    r, g, b = colors.get_color(ci)
466                    ctf.AddRGBPoint(x, r, g, b)
467                    # colors.printc('color at', round(x, 1),
468                    #               'set to', colors.get_color_name((r, g, b)), bold=0)
469            else:
470                # user passing [color1, color2, ..]
471                for i, ci in enumerate(col):
472                    r, g, b = colors.get_color(ci)
473                    x = vmin + (vmax - vmin) * i / (len(col) - 1)
474                    ctf.AddRGBPoint(x, r, g, b)
475        elif isinstance(col, str):
476            if col in colors.colors.keys() or col in colors.color_nicks.keys():
477                r, g, b = colors.get_color(col)
478                ctf.AddRGBPoint(vmin, r, g, b)  # constant color
479                ctf.AddRGBPoint(vmax, r, g, b)
480            else:  # assume it's a colormap
481                for x in np.linspace(vmin, vmax, num=64, endpoint=True):
482                    r, g, b = colors.color_map(x, name=col, vmin=vmin, vmax=vmax)
483                    ctf.AddRGBPoint(x, r, g, b)
484        elif isinstance(col, int):
485            r, g, b = colors.get_color(col)
486            ctf.AddRGBPoint(vmin, r, g, b)  # constant color
487            ctf.AddRGBPoint(vmax, r, g, b)
488        else:
489            vedo.logger.warning(f"in color() unknown input type {type(col)}")
490
491        if alpha is not None:
492            self.alpha(alpha, vmin=vmin, vmax=vmax)
493        return self

Assign a color or a set of colors along the range of the scalar value. A single constant color can also be assigned. Any matplotlib color map name is also accepted, e.g. volume.color('jet').

E.g.: say that your cells scalar runs from -3 to 6, and you want -3 to show red and 1.5 violet and 6 green, then just set:

volume.color(['red', 'violet', 'green'])

You can also assign a specific color to a aspecific value with eg.:

volume.color([(0,'red'), (0.5,'violet'), (1,'green')])

Arguments:
  • alpha : (list) use a list to specify transparencies along the scalar range
  • vmin : (float) force the min of the scalar range to be this value
  • vmax : (float) force the max of the scalar range to be this value
def alpha(self, alpha, vmin=None, vmax=None) -> Self:
495    def alpha(self, alpha, vmin=None, vmax=None) -> Self:
496        """
497        Assign a set of tranparencies along the range of the scalar value.
498        A single constant value can also be assigned.
499
500        E.g.: say `alpha=(0.0, 0.3, 0.9, 1)` and the scalar range goes from -10 to 150.
501        Then all cells with a value close to -10 will be completely transparent, cells at 1/4
502        of the range will get an alpha equal to 0.3 and voxels with value close to 150
503        will be completely opaque.
504
505        As a second option one can set explicit (x, alpha_x) pairs to define the transfer function.
506
507        E.g.: say `alpha=[(-5, 0), (35, 0.4) (123,0.9)]` and the scalar range goes from -10 to 150.
508        Then all cells below -5 will be completely transparent, cells with a scalar value of 35
509        will get an opacity of 40% and above 123 alpha is set to 90%.
510        """
511        if vmin is None:
512            vmin, _ = self.dataset.GetScalarRange()
513        if vmax is None:
514            _, vmax = self.dataset.GetScalarRange()
515        otf = self.properties.GetScalarOpacity()
516        otf.RemoveAllPoints()
517
518        if utils.is_sequence(alpha):
519            alpha = np.array(alpha)
520            if len(alpha.shape) == 1:  # user passing a flat list e.g. (0.0, 0.3, 0.9, 1)
521                for i, al in enumerate(alpha):
522                    xalpha = vmin + (vmax - vmin) * i / (len(alpha) - 1)
523                    # Create transfer mapping scalar value to opacity
524                    otf.AddPoint(xalpha, al)
525                    # print("alpha at", round(xalpha, 1), "\tset to", al)
526            elif len(alpha.shape) == 2:  # user passing [(x0,alpha0), ...]
527                otf.AddPoint(vmin, alpha[0][1])
528                for xalpha, al in alpha:
529                    # Create transfer mapping scalar value to opacity
530                    otf.AddPoint(xalpha, al)
531                otf.AddPoint(vmax, alpha[-1][1])
532
533        else:
534
535            otf.AddPoint(vmin, alpha)  # constant alpha
536            otf.AddPoint(vmax, alpha)
537
538        return self

Assign a set of tranparencies along the range of the scalar value. A single constant value can also be assigned.

E.g.: say alpha=(0.0, 0.3, 0.9, 1) and the scalar range goes from -10 to 150. Then all cells with a value close to -10 will be completely transparent, cells at 1/4 of the range will get an alpha equal to 0.3 and voxels with value close to 150 will be completely opaque.

As a second option one can set explicit (x, alpha_x) pairs to define the transfer function.

E.g.: say alpha=[(-5, 0), (35, 0.4) (123,0.9)] and the scalar range goes from -10 to 150. Then all cells below -5 will be completely transparent, cells with a scalar value of 35 will get an opacity of 40% and above 123 alpha is set to 90%.

class PointsVisual(CommonVisual):
 813class PointsVisual(CommonVisual):
 814    """Class to manage the visual aspects of a ``Points`` object."""
 815
 816    def __init__(self):
 817        # print("init PointsVisual")
 818        super().__init__()
 819        self.properties_backface = None
 820        self._cmap_name = None
 821        self.trail = None
 822        self.trail_offset = 0
 823        self.trail_points = []
 824        self._caption = None
 825        
 826
 827    def clone2d(self, size=None, offset=(), scale=None):
 828        """
 829        Turn a 3D `Points` or `Mesh` into a flat 2D actor.
 830        Returns a `Actor2D`.
 831
 832        Arguments:
 833            size : (float)
 834                size as scaling factor for the 2D actor
 835            offset : (list)
 836                2D (x, y) position of the actor in the range [-1, 1]
 837            scale : (float)
 838                Deprecated. Use `size` instead.
 839
 840        Examples:
 841            - [clone2d.py](https://github.com/marcomusy/vedo/tree/master/examples/other/clone2d.py)
 842
 843                ![](https://vedo.embl.es/images/other/clone2d.png)
 844        """
 845        # assembly.Assembly.clone2d() superseeds this method
 846        if scale is not None:
 847            vedo.logger.warning("clone2d(): use keyword size not scale")
 848            size = scale
 849
 850        if size is None:
 851            # work out a reasonable scale
 852            msiz = self.diagonal_size()
 853            if vedo.plotter_instance and vedo.plotter_instance.window:
 854                sz = vedo.plotter_instance.window.GetSize()
 855                dsiz = utils.mag(sz)
 856                size = dsiz / msiz / 10
 857            else:
 858                size = 350 / msiz
 859
 860        tp = vtki.new("TransformPolyDataFilter")
 861        transform = vtki.vtkTransform()
 862        transform.Scale(size, size, size)
 863        if len(offset) == 0:
 864            offset = self.pos()
 865        transform.Translate(-utils.make3d(offset))
 866        tp.SetTransform(transform)
 867        tp.SetInputData(self.dataset)
 868        tp.Update()
 869        poly = tp.GetOutput()
 870
 871        cm = self.mapper.GetColorMode()
 872        lut = self.mapper.GetLookupTable()
 873        sv = self.mapper.GetScalarVisibility()
 874        use_lut = self.mapper.GetUseLookupTableScalarRange()
 875        vrange = self.mapper.GetScalarRange()
 876        sm = self.mapper.GetScalarMode()
 877
 878        act2d = Actor2D()
 879        act2d.dataset = poly
 880        mapper2d = vtki.new("PolyDataMapper2D")
 881        mapper2d.SetInputData(poly)
 882        mapper2d.SetColorMode(cm)
 883        mapper2d.SetLookupTable(lut)
 884        mapper2d.SetScalarVisibility(sv)
 885        mapper2d.SetUseLookupTableScalarRange(use_lut)
 886        mapper2d.SetScalarRange(vrange)
 887        mapper2d.SetScalarMode(sm)
 888        act2d.mapper = mapper2d
 889
 890        act2d.GetPositionCoordinate().SetCoordinateSystem(4)
 891        act2d.properties.SetColor(self.color())
 892        act2d.properties.SetOpacity(self.alpha())
 893        act2d.properties.SetLineWidth(self.properties.GetLineWidth())
 894        act2d.properties.SetPointSize(self.properties.GetPointSize())
 895        act2d.properties.SetDisplayLocation(0) # 0 = back, 1 = front
 896        act2d.PickableOff()
 897        return act2d
 898
 899    ##################################################
 900    def copy_properties_from(self, source, deep=True, actor_related=True) -> Self:
 901        """
 902        Copy properties from another ``Points`` object.
 903        """
 904        pr = vtki.vtkProperty()
 905        try:
 906            sp = source.properties
 907            mp = source.mapper
 908            sa = source.actor
 909        except AttributeError:
 910            sp = source.GetProperty()
 911            mp = source.GetMapper()
 912            sa = source
 913            
 914        if deep:
 915            pr.DeepCopy(sp)
 916        else:
 917            pr.ShallowCopy(sp)
 918        self.actor.SetProperty(pr)
 919        self.properties = pr
 920
 921        if self.actor.GetBackfaceProperty():
 922            bfpr = vtki.vtkProperty()
 923            bfpr.DeepCopy(sa.GetBackfaceProperty())
 924            self.actor.SetBackfaceProperty(bfpr)
 925            self.properties_backface = bfpr
 926
 927        if not actor_related:
 928            return self
 929
 930        # mapper related:
 931        self.mapper.SetScalarVisibility(mp.GetScalarVisibility())
 932        self.mapper.SetScalarMode(mp.GetScalarMode())
 933        self.mapper.SetScalarRange(mp.GetScalarRange())
 934        self.mapper.SetLookupTable(mp.GetLookupTable())
 935        self.mapper.SetColorMode(mp.GetColorMode())
 936        self.mapper.SetInterpolateScalarsBeforeMapping(
 937            mp.GetInterpolateScalarsBeforeMapping()
 938        )
 939        self.mapper.SetUseLookupTableScalarRange(
 940            mp.GetUseLookupTableScalarRange()
 941        )
 942
 943        self.actor.SetPickable(sa.GetPickable())
 944        self.actor.SetDragable(sa.GetDragable())
 945        self.actor.SetTexture(sa.GetTexture())
 946        self.actor.SetVisibility(sa.GetVisibility())
 947        return self
 948
 949    def color(self, c=False, alpha=None) -> Union[np.ndarray, Self]:
 950        """
 951        Set/get mesh's color.
 952        If None is passed as input, will use colors from active scalars.
 953        Same as `mesh.c()`.
 954        """
 955        if c is False:
 956            return np.array(self.properties.GetColor())
 957        if c is None:
 958            self.mapper.ScalarVisibilityOn()
 959            return self
 960        self.mapper.ScalarVisibilityOff()
 961        cc = colors.get_color(c)
 962        self.properties.SetColor(cc)
 963        if self.trail:
 964            self.trail.properties.SetColor(cc)
 965        if alpha is not None:
 966            self.alpha(alpha)
 967        return self
 968
 969    def c(self, color=False, alpha=None) -> Union[np.ndarray, Self]:
 970        """
 971        Shortcut for `color()`.
 972        If None is passed as input, will use colors from current active scalars.
 973        """
 974        return self.color(color, alpha)
 975
 976    def alpha(self, opacity=None) -> Union[float, Self]:
 977        """Set/get mesh's transparency. Same as `mesh.opacity()`."""
 978        if opacity is None:
 979            return self.properties.GetOpacity()
 980
 981        self.properties.SetOpacity(opacity)
 982        bfp = self.actor.GetBackfaceProperty()
 983        if bfp:
 984            if opacity < 1:
 985                self.properties_backface = bfp
 986                self.actor.SetBackfaceProperty(None)
 987            else:
 988                self.actor.SetBackfaceProperty(self.properties_backface)
 989        return self
 990
 991    def lut_color_at(self, value) -> np.ndarray:
 992        """
 993        Return the color and alpha in the lookup table at given value.
 994        """
 995        lut = self.mapper.GetLookupTable()
 996        if not lut:
 997            return None
 998        rgb = [0,0,0]
 999        lut.GetColor(value, rgb)
1000        alpha = lut.GetOpacity(value)
1001        return np.array(rgb + [alpha])
1002
1003    def opacity(self, alpha=None) -> Union[float, Self]:
1004        """Set/get mesh's transparency. Same as `mesh.alpha()`."""
1005        return self.alpha(alpha)
1006
1007    def force_opaque(self, value=True) -> Self:
1008        """ Force the Mesh, Line or point cloud to be treated as opaque"""
1009        ## force the opaque pass, fixes picking in vtk9
1010        # but causes other bad troubles with lines..
1011        self.actor.SetForceOpaque(value)
1012        return self
1013
1014    def force_translucent(self, value=True) -> Self:
1015        """ Force the Mesh, Line or point cloud to be treated as translucent"""
1016        self.actor.SetForceTranslucent(value)
1017        return self
1018
1019    def point_size(self, value=None) -> Union[int, Self]:
1020        """Set/get mesh's point size of vertices. Same as `mesh.ps()`"""
1021        if value is None:
1022            return self.properties.GetPointSize()
1023            # self.properties.SetRepresentationToSurface()
1024        else:
1025            self.properties.SetRepresentationToPoints()
1026            self.properties.SetPointSize(value)
1027        return self
1028
1029    def ps(self, pointsize=None) -> Union[int, Self]:
1030        """Set/get mesh's point size of vertices. Same as `mesh.point_size()`"""
1031        return self.point_size(pointsize)
1032
1033    def render_points_as_spheres(self, value=True) -> Self:
1034        """Make points look spheric or else make them look as squares."""
1035        self.properties.SetRenderPointsAsSpheres(value)
1036        return self
1037
1038    def lighting(
1039        self,
1040        style="",
1041        ambient=None,
1042        diffuse=None,
1043        specular=None,
1044        specular_power=None,
1045        specular_color=None,
1046        metallicity=None,
1047        roughness=None,
1048    ) -> Self:
1049        """
1050        Set the ambient, diffuse, specular and specular_power lighting constants.
1051
1052        Arguments:
1053            style : (str)
1054                preset style, options are `[metallic, plastic, shiny, glossy, ambient, off]`
1055            ambient : (float)
1056                ambient fraction of emission [0-1]
1057            diffuse : (float)
1058                emission of diffused light in fraction [0-1]
1059            specular : (float)
1060                fraction of reflected light [0-1]
1061            specular_power : (float)
1062                precision of reflection [1-100]
1063            specular_color : (color)
1064                color that is being reflected by the surface
1065
1066        <img src="https://upload.wikimedia.org/wikipedia/commons/6/6b/Phong_components_version_4.png" alt="", width=700px>
1067
1068        Examples:
1069            - [specular.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/specular.py)
1070        """
1071        pr = self.properties
1072
1073        if style:
1074
1075            if style != "off":
1076                pr.LightingOn()
1077
1078            if style == "off":
1079                pr.SetInterpolationToFlat()
1080                pr.LightingOff()
1081                return self  ##############
1082
1083            if hasattr(pr, "GetColor"):  # could be Volume
1084                c = pr.GetColor()
1085            else:
1086                c = (1, 1, 0.99)
1087            mpr = self.mapper
1088            if hasattr(mpr, 'GetScalarVisibility') and mpr.GetScalarVisibility():
1089                c = (1,1,0.99)
1090            if   style=='metallic': pars = [0.1, 0.3, 1.0, 10, c]
1091            elif style=='plastic' : pars = [0.3, 0.4, 0.3,  5, c]
1092            elif style=='shiny'   : pars = [0.2, 0.6, 0.8, 50, c]
1093            elif style=='glossy'  : pars = [0.1, 0.7, 0.9,