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

Class to manage the visual aspects common to all objects.

CommonVisual()
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
def print(self):
62    def print(self):
63        """Print object info."""
64        print(self.__str__())
65        return self

Print object info.

LUT: numpy.ndarray
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)

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

def scalar_range(self, vmin=None, vmax=None):
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

Set the range of the scalar value for visualization.

def add_observer(self, event_name, func, priority=0) -> int:
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

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

def invoke_event(self, event_name) -> Self:
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

Invoke an event.

def show(self, **options) -> Optional[vedo.plotter.Plotter]:
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)

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:
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

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

def pickable(self, value=None) -> Self:
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

Set/get the pickability property of an object.

def use_bounds(self, value=True) -> Self:
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

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

def draggable(self, value=None) -> Self:
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

Set/get the draggability property of an object.

def on(self) -> Self:
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

Switch on object visibility. Object is not removed.

def off(self) -> Self:
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

Switch off object visibility. Object is not removed.

def toggle(self) -> Self:
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

Toggle object visibility on/off.

def add_scalarbar( self, title='', pos=(0.775, 0.05), title_yoffset=15, font_size=12, size=(60, 350), nlabels=None, c=None, horizontal=False, use_alpha=True, label_format=':6.3g') -> Self:
254    def add_scalarbar(
255        self,
256        title="",
257        pos=(0.775, 0.05),
258        title_yoffset=15,
259        font_size=12,
260        size=(60, 350),
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 obj.
269
270        Arguments:
271            title : (str)
272                scalar bar title
273            pos : (float,float)
274                position coordinates of the bottom left corner
275            title_yoffset : (float)
276                vertical space offset between title and color scalarbar
277            font_size : (float)
278                size of font for title and numeric labels
279            size : (float,float)
280                size of the scalarbar in number of pixels (width, height)
281            nlabels : (int)
282                number of numeric labels
283            c : (list)
284                color of the scalar bar text
285            horizontal : (bool)
286                lay the scalarbar horizontally
287            use_alpha : (bool)
288                render transparency in the color bar itself
289            label_format : (str)
290                c-style format string for numeric labels
291
292        Examples:
293            - [mesh_coloring.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mesh_coloring.py)
294            - [scalarbars.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/scalarbars.py)
295        """
296        plt = vedo.plotter_instance
297
298        if plt and plt.renderer:
299            c = (0.9, 0.9, 0.9)
300            if np.sum(plt.renderer.GetBackground()) > 1.5:
301                c = (0.1, 0.1, 0.1)
302            if isinstance(self.scalarbar, vtki.vtkActor):
303                plt.renderer.RemoveActor(self.scalarbar)
304            elif isinstance(self.scalarbar, vedo.Assembly):
305                for a in self.scalarbar.unpack():
306                    plt.renderer.RemoveActor(a)
307        if c is None:
308            c = "gray"
309
310        sb = vedo.addons.ScalarBar(
311            self,
312            title,
313            pos,
314            title_yoffset,
315            font_size,
316            size,
317            nlabels,
318            c,
319            horizontal,
320            use_alpha,
321            label_format,
322        )
323        self.scalarbar = sb
324        return self

Add a 2D scalar bar for the specified obj.

Arguments:
  • title : (str) scalar bar title
  • pos : (float,float) position coordinates of the bottom left corner
  • title_yoffset : (float) vertical space offset between title and color scalarbar
  • font_size : (float) size of font for title and numeric labels
  • size : (float,float) size of the scalarbar in number of pixels (width, height)
  • 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:
326    def add_scalarbar3d(
327        self,
328        title="",
329        pos=None,
330        size=(0, 0),
331        title_font="",
332        title_xoffset=-1.2,
333        title_yoffset=0.0,
334        title_size=1.5,
335        title_rotation=0.0,
336        nlabels=9,
337        label_font="",
338        label_size=1,
339        label_offset=0.375,
340        label_rotation=0,
341        label_format="",
342        italic=0,
343        c=None,
344        draw_box=True,
345        above_text=None,
346        below_text=None,
347        nan_text="NaN",
348        categories=None,
349    ) -> Self:
350        """
351        Associate a 3D scalar bar to the object and add it to the scene.
352        The new scalarbar object (Assembly) will be accessible as obj.scalarbar
353
354        Arguments:
355            size : (list)
356                (thickness, length) of scalarbar
357            title : (str)
358                scalar bar title
359            title_xoffset : (float)
360                horizontal space btw title and color scalarbar
361            title_yoffset : (float)
362                vertical space offset
363            title_size : (float)
364                size of title wrt numeric labels
365            title_rotation : (float)
366                title rotation in degrees
367            nlabels : (int)
368                number of numeric labels
369            label_font : (str)
370                font type for labels
371            label_size : (float)
372                label scale factor
373            label_offset : (float)
374                space btw numeric labels and scale
375            label_rotation : (float)
376                label rotation in degrees
377            label_format : (str)
378                label format for floats and integers (e.g. `':.2f'`)
379            draw_box : (bool)
380                draw a box around the colorbar
381            categories : (list)
382                make a categorical scalarbar,
383                the input list will have the format `[value, color, alpha, textlabel]`
384
385        Examples:
386            - [scalarbars.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/scalarbars.py)
387        """
388        plt = vedo.plotter_instance
389        if plt and c is None:  # automatic black or white
390            c = (0.9, 0.9, 0.9)
391            if np.sum(vedo.get_color(plt.backgrcol)) > 1.5:
392                c = (0.1, 0.1, 0.1)
393        if c is None:
394            c = (0, 0, 0)
395        c = vedo.get_color(c)
396
397        self.scalarbar = vedo.addons.ScalarBar3D(
398            self,
399            title,
400            pos,
401            size,
402            title_font,
403            title_xoffset,
404            title_yoffset,
405            title_size,
406            title_rotation,
407            nlabels,
408            label_font,
409            label_size,
410            label_offset,
411            label_rotation,
412            label_format,
413            italic,
414            c,
415            draw_box,
416            above_text,
417            below_text,
418            nan_text,
419            categories,
420        )
421        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):
423    def color(self, col, alpha=None, vmin=None, vmax=None):
424        """
425        Assign a color or a set of colors along the range of the scalar value.
426        A single constant color can also be assigned.
427        Any matplotlib color map name is also accepted, e.g. `volume.color('jet')`.
428
429        E.g.: say that your cells scalar runs from -3 to 6,
430        and you want -3 to show red and 1.5 violet and 6 green, then just set:
431
432        `volume.color(['red', 'violet', 'green'])`
433
434        You can also assign a specific color to a aspecific value with eg.:
435
436        `volume.color([(0,'red'), (0.5,'violet'), (1,'green')])`
437
438        Arguments:
439            alpha : (list)
440                use a list to specify transparencies along the scalar range
441            vmin : (float)
442                force the min of the scalar range to be this value
443            vmax : (float)
444                force the max of the scalar range to be this value
445        """
446        # supersedes method in Points, Mesh
447
448        if col is None:
449            return self
450
451        if vmin is None:
452            vmin, _ = self.dataset.GetScalarRange()
453        if vmax is None:
454            _, vmax = self.dataset.GetScalarRange()
455        ctf = self.properties.GetRGBTransferFunction()
456        ctf.RemoveAllPoints()
457
458        if utils.is_sequence(col):
459            if utils.is_sequence(col[0]) and len(col[0]) == 2:
460                # user passing [(value1, color1), ...]
461                for x, ci in col:
462                    r, g, b = colors.get_color(ci)
463                    ctf.AddRGBPoint(x, r, g, b)
464                    # colors.printc('color at', round(x, 1),
465                    #               'set to', colors.get_color_name((r, g, b)), bold=0)
466            else:
467                # user passing [color1, color2, ..]
468                for i, ci in enumerate(col):
469                    r, g, b = colors.get_color(ci)
470                    x = vmin + (vmax - vmin) * i / (len(col) - 1)
471                    ctf.AddRGBPoint(x, r, g, b)
472        elif isinstance(col, str):
473            if col in colors.colors.keys() or col in colors.color_nicks.keys():
474                r, g, b = colors.get_color(col)
475                ctf.AddRGBPoint(vmin, r, g, b)  # constant color
476                ctf.AddRGBPoint(vmax, r, g, b)
477            else:  # assume it's a colormap
478                for x in np.linspace(vmin, vmax, num=64, endpoint=True):
479                    r, g, b = colors.color_map(x, name=col, vmin=vmin, vmax=vmax)
480                    ctf.AddRGBPoint(x, r, g, b)
481        elif isinstance(col, int):
482            r, g, b = colors.get_color(col)
483            ctf.AddRGBPoint(vmin, r, g, b)  # constant color
484            ctf.AddRGBPoint(vmax, r, g, b)
485        else:
486            vedo.logger.warning(f"in color() unknown input type {type(col)}")
487
488        if alpha is not None:
489            self.alpha(alpha, vmin=vmin, vmax=vmax)
490        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:
492    def alpha(self, alpha, vmin=None, vmax=None) -> Self:
493        """
494        Assign a set of tranparencies along the range of the scalar value.
495        A single constant value can also be assigned.
496
497        E.g.: say `alpha=(0.0, 0.3, 0.9, 1)` and the scalar range goes from -10 to 150.
498        Then all cells with a value close to -10 will be completely transparent, cells at 1/4
499        of the range will get an alpha equal to 0.3 and voxels with value close to 150
500        will be completely opaque.
501
502        As a second option one can set explicit (x, alpha_x) pairs to define the transfer function.
503
504        E.g.: say `alpha=[(-5, 0), (35, 0.4) (123,0.9)]` and the scalar range goes from -10 to 150.
505        Then all cells below -5 will be completely transparent, cells with a scalar value of 35
506        will get an opacity of 40% and above 123 alpha is set to 90%.
507        """
508        if vmin is None:
509            vmin, _ = self.dataset.GetScalarRange()
510        if vmax is None:
511            _, vmax = self.dataset.GetScalarRange()
512        otf = self.properties.GetScalarOpacity()
513        otf.RemoveAllPoints()
514
515        if utils.is_sequence(alpha):
516            alpha = np.array(alpha)
517            if len(alpha.shape) == 1:  # user passing a flat list e.g. (0.0, 0.3, 0.9, 1)
518                for i, al in enumerate(alpha):
519                    xalpha = vmin + (vmax - vmin) * i / (len(alpha) - 1)
520                    # Create transfer mapping scalar value to opacity
521                    otf.AddPoint(xalpha, al)
522                    # print("alpha at", round(xalpha, 1), "\tset to", al)
523            elif len(alpha.shape) == 2:  # user passing [(x0,alpha0), ...]
524                otf.AddPoint(vmin, alpha[0][1])
525                for xalpha, al in alpha:
526                    # Create transfer mapping scalar value to opacity
527                    otf.AddPoint(xalpha, al)
528                otf.AddPoint(vmax, alpha[-1][1])
529
530        else:
531
532            otf.AddPoint(vmin, alpha)  # constant alpha
533            otf.AddPoint(vmax, alpha)
534
535        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):
 811class PointsVisual(CommonVisual):
 812    """Class to manage the visual aspects of a ``Points`` object."""
 813
 814    def __init__(self):
 815        # print("init PointsVisual")
 816        super().__init__()
 817        self.properties_backface = None
 818        self._cmap_name = None
 819        self.trail = None
 820        self.trail_offset = 0
 821        self.trail_points = []
 822        self._caption = None
 823        
 824
 825    def clone2d(self, size=None, offset=(), scale=None):
 826        """
 827        Turn a 3D `Points` or `Mesh` into a flat 2D actor.
 828        Returns a `Actor2D`.
 829
 830        Arguments:
 831            size : (float)
 832                size as scaling factor for the 2D actor
 833            offset : (list)
 834                2D (x, y) position of the actor in the range [-1, 1]
 835            scale : (float)
 836                Deprecated. Use `size` instead.
 837
 838        Examples:
 839            - [clone2d.py](https://github.com/marcomusy/vedo/tree/master/examples/other/clone2d.py)
 840
 841                ![](https://vedo.embl.es/images/other/clone2d.png)
 842        """
 843        # assembly.Assembly.clone2d() superseeds this method
 844        if scale is not None:
 845            vedo.logger.warning("clone2d(): use keyword size not scale")
 846            size = scale
 847
 848        if size is None:
 849            # work out a reasonable scale
 850            msiz = self.diagonal_size()
 851            if vedo.plotter_instance and vedo.plotter_instance.window:
 852                sz = vedo.plotter_instance.window.GetSize()
 853                dsiz = utils.mag(sz)
 854                size = dsiz / msiz / 10
 855            else:
 856                size = 350 / msiz
 857
 858        tp = vtki.new("TransformPolyDataFilter")
 859        transform = vtki.vtkTransform()
 860        transform.Scale(size, size, size)
 861        if len(offset) == 0:
 862            offset = self.pos()
 863        transform.Translate(-utils.make3d(offset))
 864        tp.SetTransform(transform)
 865        tp.SetInputData(self.dataset)
 866        tp.Update()
 867        poly = tp.GetOutput()
 868
 869        cm = self.mapper.GetColorMode()
 870        lut = self.mapper.GetLookupTable()
 871        sv = self.mapper.GetScalarVisibility()
 872        use_lut = self.mapper.GetUseLookupTableScalarRange()
 873        vrange = self.mapper.GetScalarRange()
 874        sm = self.mapper.GetScalarMode()
 875
 876        act2d = Actor2D()
 877        act2d.dataset = poly
 878        mapper2d = vtki.new("PolyDataMapper2D")
 879        mapper2d.SetInputData(poly)
 880        mapper2d.SetColorMode(cm)
 881        mapper2d.SetLookupTable(lut)
 882        mapper2d.SetScalarVisibility(sv)
 883        mapper2d.SetUseLookupTableScalarRange(use_lut)
 884        mapper2d.SetScalarRange(vrange)
 885        mapper2d.SetScalarMode(sm)
 886        act2d.mapper = mapper2d
 887
 888        act2d.GetPositionCoordinate().SetCoordinateSystem(4)
 889        act2d.properties.SetColor(self.color())
 890        act2d.properties.SetOpacity(self.alpha())
 891        act2d.properties.SetLineWidth(self.properties.GetLineWidth())
 892        act2d.properties.SetPointSize(self.properties.GetPointSize())
 893        act2d.properties.SetDisplayLocation(0) # 0 = back, 1 = front
 894        act2d.PickableOff()
 895        return act2d
 896
 897    ##################################################
 898    def copy_properties_from(self, source, deep=True, actor_related=True) -> Self:
 899        """
 900        Copy properties from another ``Points`` object.
 901        """
 902        pr = vtki.vtkProperty()
 903        try:
 904            sp = source.properties
 905            mp = source.mapper
 906            sa = source.actor
 907        except AttributeError:
 908            sp = source.GetProperty()
 909            mp = source.GetMapper()
 910            sa = source
 911            
 912        if deep:
 913            pr.DeepCopy(sp)
 914        else:
 915            pr.ShallowCopy(sp)
 916        self.actor.SetProperty(pr)
 917        self.properties = pr
 918
 919        if self.actor.GetBackfaceProperty():
 920            bfpr = vtki.vtkProperty()
 921            bfpr.DeepCopy(sa.GetBackfaceProperty())
 922            self.actor.SetBackfaceProperty(bfpr)
 923            self.properties_backface = bfpr
 924
 925        if not actor_related:
 926            return self
 927
 928        # mapper related:
 929        self.mapper.SetScalarVisibility(mp.GetScalarVisibility())
 930        self.mapper.SetScalarMode(mp.GetScalarMode())
 931        self.mapper.SetScalarRange(mp.GetScalarRange())
 932        self.mapper.SetLookupTable(mp.GetLookupTable())
 933        self.mapper.SetColorMode(mp.GetColorMode())
 934        self.mapper.SetInterpolateScalarsBeforeMapping(
 935            mp.GetInterpolateScalarsBeforeMapping()
 936        )
 937        self.mapper.SetUseLookupTableScalarRange(
 938            mp.GetUseLookupTableScalarRange()
 939        )
 940
 941        self.actor.SetPickable(sa.GetPickable())
 942        self.actor.SetDragable(sa.GetDragable())
 943        self.actor.SetTexture(sa.GetTexture())
 944        self.actor.SetVisibility(sa.GetVisibility())
 945        return self
 946
 947    def color(self, c=False, alpha=None) -> Union[np.ndarray, Self]:
 948        """
 949        Set/get mesh's color.
 950        If None is passed as input, will use colors from active scalars.
 951        Same as `mesh.c()`.
 952        """
 953        if c is False:
 954            return np.array(self.properties.GetColor())
 955        if c is None:
 956            self.mapper.ScalarVisibilityOn()
 957            return self
 958        self.mapper.ScalarVisibilityOff()
 959        cc = colors.get_color(c)
 960        self.properties.SetColor(cc)
 961        if self.trail:
 962            self.trail.properties.SetColor(cc)
 963        if alpha is not None:
 964            self.alpha(alpha)
 965        return self
 966
 967    def c(self, color=False, alpha=None) -> Union[np.ndarray, Self]:
 968        """
 969        Shortcut for `color()`.
 970        If None is passed as input, will use colors from current active scalars.
 971        """
 972        return self.color(color, alpha)
 973
 974    def alpha(self, opacity=None) -> Union[float, Self]:
 975        """Set/get mesh's transparency. Same as `mesh.opacity()`."""
 976        if opacity is None:
 977            return self.properties.GetOpacity()
 978
 979        self.properties.SetOpacity(opacity)
 980        bfp = self.actor.GetBackfaceProperty()
 981        if bfp:
 982            if opacity < 1:
 983                self.properties_backface = bfp
 984                self.actor.SetBackfaceProperty(None)
 985            else:
 986                self.actor.SetBackfaceProperty(self.properties_backface)
 987        return self
 988
 989    def lut_color_at(self, value) -> np.ndarray:
 990        """
 991        Return the color and alpha in the lookup table at given value.
 992        """
 993        lut = self.mapper.GetLookupTable()
 994        if not lut:
 995            return None
 996        rgb = [0,0,0]
 997        lut.GetColor(value, rgb)
 998        alpha = lut.GetOpacity(value)
 999        return np.array(rgb + [alpha])
1000
1001    def opacity(self, alpha=None) -> Union[float, Self]:
1002        """Set/get mesh's transparency. Same as `mesh.alpha()`."""
1003        return self.alpha(alpha)
1004
1005    def force_opaque(self, value=True) -> Self:
1006        """ Force the Mesh, Line or point cloud to be treated as opaque"""
1007        ## force the opaque pass, fixes picking in vtk9
1008        # but causes other bad troubles with lines..
1009        self.actor.SetForceOpaque(value)
1010        return self
1011
1012    def force_translucent(self, value=True) -> Self:
1013        """ Force the Mesh, Line or point cloud to be treated as translucent"""
1014        self.actor.SetForceTranslucent(value)
1015        return self
1016
1017    def point_size(self, value=None) -> Union[int, Self]:
1018        """Set/get mesh's point size of vertices. Same as `mesh.ps()`"""
1019        if value is None:
1020            return self.properties.GetPointSize()
1021            # self.properties.SetRepresentationToSurface()
1022        else:
1023            self.properties.SetRepresentationToPoints()
1024            self.properties.SetPointSize(value)
1025        return self
1026
1027    def ps(self, pointsize=None) -> Union[int, Self]:
1028        """Set/get mesh's point size of vertices. Same as `mesh.point_size()`"""
1029        return self.point_size(pointsize)
1030
1031    def render_points_as_spheres(self, value=True) -> Self:
1032        """Make points look spheric or else make them look as squares."""
1033        self.properties.SetRenderPointsAsSpheres(value)
1034        return self
1035
1036    def lighting(
1037        self,
1038        style="",
1039        ambient=None,
1040        diffuse=None,
1041        specular=None,
1042        specular_power=None,
1043        specular_color=None,
1044        metallicity=None,
1045        roughness=None,
1046    ) -> Self:
1047        """
1048        Set the ambient, diffuse, specular and specular_power lighting constants.
1049
1050        Arguments:
1051            style : (str)
1052                preset style, options are `[metallic, plastic, shiny, glossy, ambient, off]`
1053            ambient : (float)
1054                ambient fraction of emission [0-1]
1055            diffuse : (float)
1056                emission of diffused light in fraction [0-1]
1057            specular : (float)
1058                fraction of reflected light [0-1]
1059            specular_power : (float)
1060                precision of reflection [1-100]
1061            specular_color : (color)
1062                color that is being reflected by the surface
1063
1064        <img src="https://upload.wikimedia.org/wikipedia/commons/6/6b/Phong_components_version_4.png" alt="", width=700px>
1065
1066        Examples:
1067            - [specular.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/specular.py)
1068        """
1069        pr = self.properties
1070
1071        if style:
1072
1073            if style != "off":
1074                pr.LightingOn()
1075
1076            if style == "off":
1077                pr.SetInterpolationToFlat()
1078                pr.LightingOff()
1079                return self  ##############
1080
1081            if hasattr(pr, "GetColor"):  # could be Volume
1082                c = pr.GetColor()
1083            else:
1084                c = (1, 1, 0.99)
1085            mpr = self.mapper
1086            if hasattr(mpr, 'GetScalarVisibility') and mpr.GetScalarVisibility():
1087                c = (1,1,0.99)
1088            if   style=='metallic': pars = [0.1, 0.3, 1.0, 10, c]
1089            elif style=='plastic' : pars = [0.3, 0.4, 0.3,  5, c]
1090            elif style=='shiny'   : pars = [0.2, 0.6, 0.8, 50, c]
1091            elif style=='glossy'  : pars = [0.1, 0.7, 0.9, 90, (1,1,0.99)]
1092            elif style=='ambient' : pars = [0.8, 0.1, 0.0,  1, (1,1,1)]
1093            elif style=='default' : pars = [0.1, 1.0, 0.05, 5, c]
1094            else:
1095                vedo.logger.error("in lighting(): Available styles are")
1096                vedo.logger.error("[default, metallic, plastic, shiny, glossy, ambient, off]")
1097                raise RuntimeError