vedo.visual

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

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

Class to manage the visual aspects common to all objects.

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

Print object info.

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

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

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

Set the range of the scalar value for visualization.

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

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

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

Invoke an event.

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

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

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

Returns the Plotter class instance.

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

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

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

Set/get the pickability property of an object.

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

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

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

Set/get the draggability property of an object.

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

Switch on object visibility. Object is not removed.

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

Switch off object visibility. Object is not removed.

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

Toggle object visibility on/off.

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

Add a 2D scalar bar for the specified object.

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

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

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

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

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

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

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

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

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

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

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

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

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

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