import time
import os
import numpy as np
import vedo
from vedo.colors import colorMap
from vedo.colors import getColor
from vedo.utils import isSequence
from vedo.utils import linInterpolate
from vedo.utils import mag
from vedo.utils import precision
from vedo.plotter import Plotter
from vedo.pointcloud import fitPlane
from vedo.pointcloud import Points
from vedo.shapes import Line
from vedo.shapes import Ribbon
from vedo.shapes import Spline
from vedo.shapes import Text2D
from vedo.pyplot import CornerHistogram

__doc__ = """
This module contains vedo applications which provide some *ready-to-use* funcionalities
.. image:: https://vedo.embl.es/images/advanced/app_raycaster.gif
"""


__all__ = [
    "Browser",
    "IsosurfaceBrowser",
    "FreeHandCutPlotter",
    "RayCastPlotter",
    "Slicer3DPlotter",
    "Slicer2DPlotter",
    "SplinePlotter",
    "Clock",
]


#################################
class Slicer3DPlotter(Plotter):
    """
    Generate a ``Plotter`` window with slicing planes for the input Volume.

    Returns the ``Plotter`` object.

    Parameters
    ----------
    alpha : float
        transparency of the slicing planes

    cmaps : list
        list of color maps names to cycle when clicking button

    map2cells : bool
        scalars are mapped to cells, not interpolated

    clamp : bool
        clamp scalar to reduce the effect of tails in color mapping

    useSlider3D : bool
        show sliders attached along the axes

    showHisto : bool
        show histogram on bottom left

    showIcon : bool
        show a small 3D rendering icon of the volume

    draggable : bool
        make the icon draggable

    .. hint:: examples/volumetric/slicer1.py
        .. image:: https://vedo.embl.es/images/volumetric/slicer1.jpg
    """

    def __init__(
            self,
            volume,
            alpha=1,
            cmaps=('gist_ncar_r', "hot_r", "bone_r", "jet", "Spectral_r"),
            map2cells=False,  # buggy
            clamp=True,
            useSlider3D=False,
            showHisto=True,
            showIcon=True,
            draggable=False,
            pos=(0, 0),
            size="auto",
            screensize="auto",
            title="",
            bg="white",
            bg2="lightblue",
            axes=7,
            resetcam=True,
            interactive=True,
        ):
        self._cmap_slicer= 'gist_ncar_r'

        if not title:
            if volume.filename:
                title = volume.filename
            else:
                title = "Volume Slicer"

        ################################
        Plotter.__init__(
            self,
            pos=pos,
            bg=bg,
            bg2=bg2,
            size=size,
            screensize=screensize,
            title=title,
            interactive=interactive,
            axes=axes,
        )
        ################################
        box = volume.box().wireframe().alpha(0.1)

        self.show(box, viewup="z", resetcam=resetcam, interactive=False)
        if showIcon:
            self.addInset(
                volume,
                pos=(0.85, 0.85),
                size=0.15, c="w",
                draggable=draggable,
            )

        # inits
        la, ld = 0.7, 0.3  # ambient, diffuse
        dims = volume.dimensions()
        data = volume.pointdata[0]
        rmin, rmax = volume.imagedata().GetScalarRange()
        if clamp:
            hdata, edg = np.histogram(data, bins=50)
            logdata = np.log(hdata + 1)
            # mean  of the logscale plot
            meanlog = np.sum(np.multiply(edg[:-1], logdata)) / np.sum(logdata)
            rmax = min(rmax, meanlog + (meanlog - rmin) * 0.9)
            rmin = max(rmin, meanlog - (rmax - meanlog) * 0.9)
            vedo.logger.debug(
                "scalar range clamped to range: ("
                + precision(rmin, 3)
                + ", "
                + precision(rmax, 3)
                + ")"
            )
        self._cmap_slicer = cmaps[0]
        visibles = [None, None, None]
        msh = volume.zSlice(int(dims[2] / 2))
        msh.alpha(alpha).lighting("", la, ld, 0)
        msh.cmap(self._cmap_slicer, vmin=rmin, vmax=rmax)
        if map2cells:
            msh.mapPointsToCells()
        self.renderer.AddActor(msh)
        visibles[2] = msh
        msh.addScalarBar(pos=(0.04, 0.0), horizontal=True, titleFontSize=0)

        def sliderfunc_x(widget, event):
            i = int(widget.GetRepresentation().GetValue())
            msh = volume.xSlice(i).alpha(alpha).lighting("", la, ld, 0)
            msh.cmap(self._cmap_slicer, vmin=rmin, vmax=rmax)
            if map2cells:
                msh.mapPointsToCells()
            self.renderer.RemoveActor(visibles[0])
            if i and i < dims[0]:
                self.renderer.AddActor(msh)
            visibles[0] = msh

        def sliderfunc_y(widget, event):
            i = int(widget.GetRepresentation().GetValue())
            msh = volume.ySlice(i).alpha(alpha).lighting("", la, ld, 0)
            msh.cmap(self._cmap_slicer, vmin=rmin, vmax=rmax)
            if map2cells:
                msh.mapPointsToCells()
            self.renderer.RemoveActor(visibles[1])
            if i and i < dims[1]:
                self.renderer.AddActor(msh)
            visibles[1] = msh

        def sliderfunc_z(widget, event):
            i = int(widget.GetRepresentation().GetValue())
            msh = volume.zSlice(i).alpha(alpha).lighting("", la, ld, 0)
            msh.cmap(self._cmap_slicer, vmin=rmin, vmax=rmax)
            if map2cells:
                msh.mapPointsToCells()
            self.renderer.RemoveActor(visibles[2])
            if i and i < dims[2]:
                self.renderer.AddActor(msh)
            visibles[2] = msh

        cx, cy, cz, ch = "dr", "dg", "db", (0.3, 0.3, 0.3)
        if np.sum(self.renderer.GetBackground()) < 1.5:
            cx, cy, cz = "lr", "lg", "lb"
            ch = (0.8, 0.8, 0.8)

        if not useSlider3D:
            self.addSlider2D(
                sliderfunc_x,
                0,
                dims[0],
                title="X",
                titleSize=0.5,
                pos=[(0.8, 0.12), (0.95, 0.12)],
                showValue=False,
                c=cx,
            )
            self.addSlider2D(
                sliderfunc_y,
                0,
                dims[1],
                title="Y",
                titleSize=0.5,
                pos=[(0.8, 0.08), (0.95, 0.08)],
                showValue=False,
                c=cy,
            )
            self.addSlider2D(
                sliderfunc_z,
                0,
                dims[2],
                title="Z",
                titleSize=0.6,
                value=int(dims[2] / 2),
                pos=[(0.8, 0.04), (0.95, 0.04)],
                showValue=False,
                c=cz,
            )
        else:  # 3d sliders attached to the axes bounds
            bs = box.bounds()
            self.addSlider3D(
                sliderfunc_x,
                pos1=(bs[0], bs[2], bs[4]),
                pos2=(bs[1], bs[2], bs[4]),
                xmin=0,
                xmax=dims[0],
                t=box.diagonalSize() / mag(box.xbounds()) * 0.6,
                c=cx,
                showValue=False,
            )
            self.addSlider3D(
                sliderfunc_y,
                pos1=(bs[1], bs[2], bs[4]),
                pos2=(bs[1], bs[3], bs[4]),
                xmin=0,
                xmax=dims[1],
                t=box.diagonalSize() / mag(box.ybounds()) * 0.6,
                c=cy,
                showValue=False,
            )
            self.addSlider3D(
                sliderfunc_z,
                pos1=(bs[0], bs[2], bs[4]),
                pos2=(bs[0], bs[2], bs[5]),
                xmin=0,
                xmax=dims[2],
                value=int(dims[2] / 2),
                t=box.diagonalSize() / mag(box.zbounds()) * 0.6,
                c=cz,
                showValue=False,
            )

        #################
        def buttonfunc():
            bu.switch()
            self._cmap_slicer = bu.status()
            for mesh in visibles:
                if mesh:
                    mesh.cmap(self._cmap_slicer, vmin=rmin, vmax=rmax)
                    if map2cells:
                        mesh.mapPointsToCells()
            self.renderer.RemoveActor(mesh.scalarbar)
            mesh.addScalarBar(pos=(0.04, 0.0), horizontal=True, titleFontSize=0)

        bu = self.addButton(
            buttonfunc,
            pos=(0.27, 0.005),
            states=cmaps,
            c=["db"] * len(cmaps),
            bc=["lb"] * len(cmaps),  # colors of states
            size=14,
            bold=True,
        )

        #################
        hist = None
        if showHisto:
            hist = CornerHistogram(
                data,
                s=0.2,
                bins=25,
                logscale=1,
                pos=(0.02, 0.02),
                c=ch,
                bg=ch,
                alpha=0.7,
            )

        self.add([msh, hist], resetcam=False)
        if interactive:
            self.interactive()


########################################################################################
class Slicer2DPlotter(Plotter):
    """
    Create a ``Plotter`` with a single slice of a Volume which always faces the camera,
    but at the same time can be oriented arbitrarily in space.

    Parameters
    ----------
    levels : list
        window and color level

    .. image:: https://vedo.embl.es/images/volumetric/read_volume3.jpg
    """

    def __init__(
        self,
        volume,
        levels=(None, None),
        axes=None,
        zoom=1.2,
        pos=(0, 0),
        size="auto",
        screensize="auto",
        title="",
        bg="white",
        bg2=None,
        interactive=True,
    ):
        custom_shape = [  # define here the 2 rendering rectangle spaces
            dict(bottomleft=(0.0, 0.0), topright=(1, 1), bg="k9"),  # the full window
            dict(bottomleft=(0.8, 0.8), topright=(1, 1), bg="k8", bg2="lb"),
        ]

        if not title:
            if volume.filename:
                title = volume.filename[:80]
            else:
                title = "Volume Slicer2D"

        Plotter.__init__(
            self,
            shape=custom_shape,
            title=title,
            pos=pos,
            screensize=screensize,
            size=size,
            bg=bg,
            bg2=bg2,
            axes=0,
            interactive=False,
        )

        vsl = vedo.volume.VolumeSlice(
            volume
        )  # reuse the same underlying data as in vol

        # no argument will grab the existing cmap in vol (or use buildLUT())
        vsl.colorize()

        if levels[0] and levels[1]:
            vsl.lighting(window=levels[0], level=levels[1])

        usage = Text2D((
            "SHIFT+Left click   \rightarrow rotate camera for oblique slicing\n"
            "SHIFT+Middle click \rightarrow slice perpendicularly through image\n"
            "Left click & drag  \rightarrow modify luminosity and contrast\n"
            "R                  \rightarrow Reset the Window/Color levels\n"
            "X                  \rightarrow Reset to sagittal view\n"
            "Y                  \rightarrow Reset to coronal view\n"
            "Z                  \rightarrow Reset to axial view"),
            font="Calco",
            pos="top-left",
            s=0.8,
            bg="yellow",
            alpha=0.25,
        )

        hist = CornerHistogram(
            volume.pointdata[0],
            bins=25,
            logscale=1,
            pos=(0.02, 0.02),
            s=0.175,
            c="dg",
            bg="k",
            alpha=1,
        )
        ax = None
        if axes == 7:
            ax = vedo.addons.RulerAxes(vsl, xtitle="x - ", ytitle="y - ", ztitle="z - ")

        box = vsl.box().alpha(0.1)
        self.show(vsl, box, ax, usage, hist, at=0, mode="image", zoom=zoom)
        self.show(volume, at=1, interactive=interactive)


########################################################################
class RayCastPlotter(Plotter):
    """
    Generate a ``Plotter`` window for Volume rendering using ray casting.

    Returns the ``Plotter`` object.

    .. hint:: examples/volumetric/app_raycaster.py
        .. image:: https://vedo.embl.es/images/advanced/app_raycaster.gif
    """

    def __init__(self, volume, **kwargs):

        Plotter.__init__(self, **kwargs)

        self.alphaslider0 = 0.33
        self.alphaslider1 = 0.66
        self.alphaslider2 = 1

        volumeProperty = volume.GetProperty()
        img = volume.imagedata()

        if volume.dimensions()[2] < 3:
            vedo.logger.error("RayCastPlotter: not enough z slices.")
            raise RuntimeError

        smin, smax = img.GetScalarRange()
        x0alpha = smin + (smax - smin) * 0.25
        x1alpha = smin + (smax - smin) * 0.5
        x2alpha = smin + (smax - smin) * 1.0

        ############################## color map slider
        # Create transfer mapping scalar value to color
        cmaps = [
            "jet",
            "viridis",
            "bone",
            "hot",
            "plasma",
            "winter",
            "cool",
            "gist_earth",
            "coolwarm",
            "tab10",
        ]
        cols_cmaps = []
        for cm in cmaps:
            cols = colorMap(range(0, 21), cm, 0, 20)  # sample 20 colors
            cols_cmaps.append(cols)
        Ncols = len(cmaps)
        csl = (0.9, 0.9, 0.9)
        if sum(getColor(self.renderer.GetBackground())) > 1.5:
            csl = (0.1, 0.1, 0.1)

        def sliderColorMap(widget, event):
            sliderRep = widget.GetRepresentation()
            k = int(sliderRep.GetValue())
            sliderRep.SetTitleText(cmaps[k])
            volume.color(cmaps[k])

        w1 = self.addSlider2D(
            sliderColorMap,
            0,
            Ncols - 1,
            value=0,
            showValue=0,
            title=cmaps[0],
            c=csl,
            pos=[(0.8, 0.05), (0.965, 0.05)],
        )
        w1.GetRepresentation().SetTitleHeight(0.018)

        ############################## alpha sliders
        # Create transfer mapping scalar value to opacity
        opacityTransferFunction = volumeProperty.GetScalarOpacity()

        def setOTF():
            opacityTransferFunction.RemoveAllPoints()
            opacityTransferFunction.AddPoint(smin, 0.0)
            opacityTransferFunction.AddPoint(smin + (smax - smin) * 0.1, 0.0)
            opacityTransferFunction.AddPoint(x0alpha, self.alphaslider0)
            opacityTransferFunction.AddPoint(x1alpha, self.alphaslider1)
            opacityTransferFunction.AddPoint(x2alpha, self.alphaslider2)

        setOTF()

        def sliderA0(widget, event):
            self.alphaslider0 = widget.GetRepresentation().GetValue()
            setOTF()

        self.addSlider2D(
            sliderA0,
            0,
            1,
            value=self.alphaslider0,
            pos=[(0.84, 0.1), (0.84, 0.26)],
            c=csl,
            showValue=0,
        )

        def sliderA1(widget, event):
            self.alphaslider1 = widget.GetRepresentation().GetValue()
            setOTF()

        self.addSlider2D(
            sliderA1,
            0,
            1,
            value=self.alphaslider1,
            pos=[(0.89, 0.1), (0.89, 0.26)],
            c=csl,
            showValue=0,
        )

        def sliderA2(widget, event):
            self.alphaslider2 = widget.GetRepresentation().GetValue()
            setOTF()

        w2 = self.addSlider2D(
            sliderA2,
            0,
            1,
            value=self.alphaslider2,
            pos=[(0.96, 0.1), (0.96, 0.26)],
            c=csl,
            showValue=0,
            title="Opacity levels",
        )
        w2.GetRepresentation().SetTitleHeight(0.016)

        # add a button
        def buttonfuncMode():
            s = volume.mode()
            snew = (s + 1) % 2
            volume.mode(snew)
            bum.switch()

        bum = self.addButton(
            buttonfuncMode,
            pos=(0.7, 0.035),
            states=["composite", "max proj."],
            c=["bb", "gray"],
            bc=["gray", "bb"],  # colors of states
            font="",
            size=16,
            bold=0,
            italic=False,
        )
        bum.status(volume.mode())

        # add histogram of scalar
        plot = CornerHistogram(
            volume,
            bins=25,
            logscale=1,
            c=(0.7, 0.7, 0.7),
            bg=(0.7, 0.7, 0.7),
            pos=(0.78, 0.065),
            lines=True,
            dots=False,
            nmax=3.1415e06,  # subsample otherwise is too slow
        )

        plot.GetPosition2Coordinate().SetValue(0.197, 0.20, 0)
        plot.GetXAxisActor2D().SetFontFactor(0.7)
        plot.GetProperty().SetOpacity(0.5)
        self.add([plot, volume])


#####################################################################################
class IsosurfaceBrowser(Plotter):
    """
    Generate a ``Plotter`` for Volume isosurfacing using a slider.

    Set ``delayed=True`` to delay slider update on mouse release.

    Set ``res`` to set the resolution, e.g. the number of desired isosurfaces to be
    generated on the fly.

    Set ``precompute=True`` to precompute the isosurfaces (so slider browsing will be smoother).

    Example:
        .. code-block:: python

            from vedo import dataurl, Volume
            from vedo.applications import IsosurfaceBrowser
            vol = Volume(dataurl+'head.vti')
            plt = IsosurfaceBrowser(vol, c='gold')
            plt.show(axes=7, bg2='lb')

    .. hint:: examples/volumetric/app_isobrowser.py
        .. image:: https://vedo.embl.es/images/advanced/app_isobrowser.gif
    """

    def __init__(
            self,
            volume,
            threshold=None,
            c=None,
            alpha=1,
            lego=False,
            res=50,
            precompute=False,
            progress=False,
            cmap='hot',
            delayed=False,
            sliderpos=4,
            pos=(0,0),
            size="auto",
            screensize="auto",
            title="",
            bg="white",
            bg2=None,
            axes=1,
            interactive=True,
        ):

        Plotter.__init__(
            self,
            pos=pos,
            bg=bg,
            bg2=bg2,
            size=size,
            screensize=screensize,
            title=title,
            interactive=interactive,
            axes=axes,
        )

        self._prev_value = 1e30

        scrange = volume.scalarRange()
        delta = scrange[1] - scrange[0]
        if not delta:
            return

        if lego:
            res = int(res / 2)  # because lego is much slower
            slidertitle = ""
        else:
            slidertitle = "threshold"

        allowed_vals = np.linspace(scrange[0], scrange[1], num=res)

        bacts = {}  # cache the meshes so we dont need to recompute
        if precompute:
            delayed = False  # no need to delay the slider in this case
            if progress:
                pb = vedo.ProgressBar(0, len(allowed_vals))

            for value in allowed_vals:
                value_name = precision(value, 2)
                if lego:
                    mesh = volume.legosurface(vmin=value)
                    if mesh.NCells():
                        mesh.cmap(cmap, vmin=scrange[0], vmax=scrange[1], on="cells")
                else:
                    mesh = volume.isosurface(threshold=value).color(c).alpha(alpha)
                bacts.update({value_name: mesh})  # store it
                if progress:
                    pb.print("isosurfacing volume..")

        ############################## threshold slider callback
        def sliderThres(widget, event):

            prevact = self.actors[0]
            if isinstance(widget, float):
                value = widget
            else:
                value = widget.GetRepresentation().GetValue()

            # snap to the closest
            idx = (np.abs(allowed_vals - value)).argmin()
            value = allowed_vals[idx]

            if abs(value - self._prev_value) / delta < 0.001:
                return
            self._prev_value = value

            value_name = precision(value, 2)
            if value_name in bacts:  # reusing the already existing mesh
                # print('reusing')
                mesh = bacts[value_name]
            else:  # else generate it
                # print('generating', value)
                if lego:
                    mesh = volume.legosurface(vmin=value)
                    if mesh.NCells():
                        mesh.cmap(cmap, vmin=scrange[0], vmax=scrange[1], on="cells")
                else:
                    mesh = volume.isosurface(threshold=value).color(c).alpha(alpha)
                bacts.update({value_name: mesh})  # store it

            self.renderer.RemoveActor(prevact)
            self.renderer.AddActor(mesh)
            self.actors[0] = mesh

        ################################################

        if threshold is None:
            threshold = delta / 3.0 + scrange[0]

        self.actors = [None]
        sliderThres(threshold, "")  # init call
        if lego:
            self.actors[0].addScalarBar(pos=(0.8, 0.12))

        self.addSlider2D(
            sliderThres,
            scrange[0] + 0.02 * delta,
            scrange[1] - 0.02 * delta,
            value=threshold,
            pos=sliderpos,
            title=slidertitle,
            showValue=True,
            delayed=delayed,
        )


##############################################################################
class Browser(Plotter):
    """
    Browse a serie of vedo objects by using a simple slider.

    Example:
        .. code-block:: python

            import vedo
            from vedo.applications import Browser
            meshes = vedo.load("data/2*0.vtk") # a python list
            plt = Browser(meshes, resetcam=1, axes=4) # a vedo.Plotter
            plt.show()

    .. hint:: examples/other/morphomatics_tube.py
    """

    def __init__(
            self,
            objects=(),
            sliderpos=((0.55, 0.07),(0.96, 0.07)),
            c=None,  # slider color
            prefix="",
            pos=(0, 0),
            size="auto",
            screensize="auto",
            title="Browser",
            bg="white",
            bg2=None,
            axes=4,
            resetcam=False,
            interactive=True,
        ):

        Plotter.__init__(
            self,
            pos=pos,
            size=size,
            screensize=screensize,
            title=title,
            bg=bg,
            bg2=bg2,
            axes=axes,
            interactive=interactive,
        )

        self += objects

        self.slider = None

        # define the slider
        def sliderfunc(widget, event=None):
            k = int(widget.GetRepresentation().GetValue())
            ak = self.actors[k]
            for a in self.actors:
                if a == ak:
                    a.on()
                else:
                    a.off()
            if resetcam:
                self.resetCamera()
            tx = str(k)
            if ak.filename:
                tx = ak.filename.split("/")[-1]
                tx = tx.split("\\")[-1]  # windows os
            elif ak.name:
                tx = ak.name
            widget.GetRepresentation().SetTitleText(prefix + tx)

        self.slider = self.addSlider2D(
            sliderfunc,
            0.5,
            len(objects) - 0.5,
            pos=sliderpos,
            font="courier",
            c=c,
            showValue=False,
        )
        self.slider.GetRepresentation().SetTitleHeight(0.020)
        sliderfunc(self.slider)  # init call


#############################################################################################
class FreeHandCutPlotter(Plotter):
    """
    A ``Plotter`` derived class which edits polygonal meshes interactively.
    Can also be invoked from command line. E.g. with:

    ``vedo --edit https://vedo.embl.es/examples/data/porsche.ply``

    Usage
    -----
        - Left-click and hold to rotate
        - Right-click and move to draw line
        - Second right-click to stop drawing
        - Press c to clear points
        -       z/Z to cut mesh (Z inverts inside-out the selection area)
        -       L to keep only the largest connected surface
        -       s to save mesh to file (tag _edited is appended to filename)
        -       u to undo last action
        -       h for help, i for info

    Parameters
    ----------
    mesh : Mesh, Points
        The input Mesh or pointcloud.

    splined : bool
        join points with a spline or a simple line.

    font : str
        Font name for the instructions.

    alpha : float
        transparency of the instruction message panel.

    lw : str
        selection line width.

    lc : str
        selection line color.

    pc : str
        selection points color.

    c : str
        backgound color of instructions.

    tc : str
        text color of instructions.

    tol : int
        tolerance of the point proximity.

    .. hint:: examples/basic/cutFreeHand.py
        .. image:: https://vedo.embl.es/images/basic/cutFreeHand.gif
    """

    # thanks to Jakub Kaminski for the original version of this script
    def __init__(
            self,
            mesh,
            splined=True,
            font="Bongas",
            alpha=0.9,
            lw=4,
            lc="red5",
            pc="red4",
            c="green3",
            tc="k9",
            tol=0.008,
            **options
        ):

        if not isinstance(mesh, Points):
            vedo.logger.error("FreeHandCutPlotter input must be Points or Mesh")
            raise RuntimeError()

        super().__init__(**options)

        self.mesh = mesh
        self.mesh_prev = mesh
        self.splined = splined
        self.linecolor = lc
        self.linewidth = lw
        self.pointcolor = pc
        self.color = c
        self.alpha = alpha

        self.msg = "Right-click and move to draw line\n"
        self.msg += "Second right-click to stop drawing\n"
        self.msg += "Press L to extract largest surface\n"
        self.msg += "        z/Z to cut mesh (s to save)\n"
        self.msg += "        c to clear points, u to undo"
        self.txt2d = Text2D(self.msg, pos="top-left", font=font, s=0.9)
        self.txt2d.c(tc).background(c, alpha).frame()

        self.idkeypress = self.addCallback("KeyPress", self._onKeyPress)
        self.idrightclck = self.addCallback("RightButton", self._onRightClick)
        self.idmousemove = self.addCallback("MouseMove", self._onMouseMove)
        self.drawmode = False
        self.tol = tol  # tolerance of point distance
        self.cpoints = []
        self.points = None
        self.spline = None
        self.jline = None
        self.topline = None
        self.top_pts = []

    def init(self, initpoints):
        """Set an initial number of points to define a region"""
        if isinstance(initpoints, Points):
            self.cpoints = initpoints.points()
        else:
            self.cpoints = np.array(initpoints)
        self.points = (
            Points(self.cpoints, r=self.linewidth).c(self.pointcolor).pickable(0)
        )
        if self.splined:
            self.spline = Spline(self.cpoints, res=len(self.cpoints) * 4)
        else:
            self.spline = Line(self.cpoints)
        self.spline.lw(self.linewidth).c(self.linecolor).pickable(False)
        self.jline = Line(
            self.cpoints[0], self.cpoints[-1], lw=1, c=self.linecolor
        ).pickable(0)
        self.add([self.points, self.spline, self.jline], render=False)
        return self

    def _onRightClick(self, evt):
        self.drawmode = not self.drawmode  # toggle mode
        if self.drawmode:
            self.txt2d.background(self.linecolor, self.alpha)
        else:
            self.txt2d.background(self.color, self.alpha)
            if len(self.cpoints) > 2:
                self.remove([self.spline, self.jline])
                if self.splined:  # show the spline closed
                    self.spline = Spline(
                        self.cpoints, closed=True, res=len(self.cpoints) * 4
                    )
                else:
                    self.spline = Line(self.cpoints, closed=True)
                self.spline.lw(self.linewidth).c(self.linecolor).pickable(False)
                self.add(self.spline)

    def _onMouseMove(self, evt):
        if self.drawmode:
            cpt = self.computeWorldPosition(evt.picked2d) # make this 2d-screen point 3d
            if self.cpoints and mag(cpt - self.cpoints[-1]) < self.mesh.diagonalSize()*self.tol:
                return  # new point is too close to the last one. skip
            self.cpoints.append(cpt)
            if len(self.cpoints) > 2:
                self.remove([self.points, self.spline, self.jline, self.topline])
                self.points = Points(self.cpoints, r=self.linewidth).c(self.pointcolor).pickable(0)
                if self.splined:
                    self.spline = Spline(self.cpoints, res=len(self.cpoints)*4) # not closed here
                else:
                    self.spline = Line(self.cpoints)

                if evt.actor:
                    self.top_pts.append(evt.picked3d)
                    # self.topline = Line(self.top_pts)
                    # self.topline.lw(self.linewidth-1).c(self.linecolor).pickable(False)
                    self.topline = Points(self.top_pts, r=self.linewidth)
                    self.topline.c(self.linecolor).pickable(False)

                self.spline.lw(self.linewidth).c(self.linecolor).pickable(False)
                self.txt2d.background(self.linecolor)
                self.jline = Line(self.cpoints[0], self.cpoints[-1], lw=1, c=self.linecolor).pickable(0)
                self.add([self.points, self.spline, self.jline, self.topline])

    def _onKeyPress(self, evt):
        if evt.keyPressed.lower() == 'z' and self.spline: # Cut mesh with a ribbon-like surface
            inv = False
            if evt.keyPressed == "Z":
                inv = True
            self.txt2d.background("red8").text("  ... working ...  ")
            self.render()
            self.mesh_prev = self.mesh.clone()
            tol = self.mesh.diagonalSize() / 2  # size of ribbon (not shown)
            pts = self.spline.points()
            n = fitPlane(pts, signed=True).normal  # compute normal vector to points
            rb = Ribbon(pts - tol * n, pts + tol * n, closed=True)
            self.mesh.cutWithMesh(rb, invert=inv)  # CUT
            self.txt2d.text(self.msg)  # put back original message
            if self.drawmode:
                self._onRightClick(evt)  # toggle mode to normal
            else:
                self.txt2d.background(self.color, self.alpha)
            self.remove([self.spline, self.points, self.jline, self.topline]).render()
            self.cpoints, self.points, self.spline = [], None, None
            self.top_pts, self.topline = [], None

        elif evt.keyPressed == "L":
            self.txt2d.background("red8")
            self.txt2d.text(" ... removing smaller ... \n ... parts of the mesh ... ")
            self.render()
            self.remove(self.mesh)
            self.mesh_prev = self.mesh
            mcut = self.mesh.extractLargestRegion()
            mcut.filename = self.mesh.filename          # copy over various properties
            mcut.name = self.mesh.name
            mcut.scalarbar = self.mesh.scalarbar
            mcut.info = self.mesh.info
            self.mesh = mcut                            # discard old mesh by overwriting it
            self.txt2d.text(self.msg).background(self.color)   # put back original message
            self.add(mcut)

        elif evt.keyPressed == 'u':                     # Undo last action
            if self.drawmode:
                self._onRightClick(evt)                 # toggle mode to normal
            else:
                self.txt2d.background(self.color, self.alpha)
            self.remove([self.mesh, self.spline, self.jline, self.points, self.topline])
            self.mesh = self.mesh_prev
            self.cpoints, self.points, self.spline = [], None, None
            self.top_pts, self.topline = [], None
            self.add(self.mesh)

        elif evt.keyPressed in ('c', 'Delete'):
            # clear all points
            self.remove([self.spline, self.points, self.jline, self.topline]).render()
            self.cpoints, self.points, self.spline = [], None, None
            self.top_pts, self.topline = [], None

        elif evt.keyPressed == "r":  # reset camera and axes
            try:
                self.remove(self.axes_instances[0])
                self.axes_instances[0] = None
                self.addGlobalAxes(axtype=1, c=None)
                self.renderer.ResetCamera()
                self.interactor.Render()
            except:
                pass

        elif evt.keyPressed == "s":
            if self.mesh.filename:
                fname = os.path.basename(self.mesh.filename)
                fname, extension = os.path.splitext(fname)
                fname = fname.replace("_edited", "")
                fname = f"{fname}_edited{extension}"
            else:
                fname = "mesh_edited.vtk"
            self.write(fname)

    def write(self, filename="mesh_edited.vtk"):
        """Save the resulting mesh to file"""
        self.mesh.write(filename)
        vedo.logger.info(f"mesh saved to file {filename}")
        return self

    def start(self, *args, **kwargs):
        """Start window interaction (with mouse and keyboard)"""
        acts = [self.txt2d, self.mesh, self.points, self.spline, self.jline]
        self.show(acts + list(args), **kwargs)
        return self

########################################################################
class SplinePlotter(Plotter):
    """
    Create an interactive application that allows the user to click points and
    retrieve the coordinates of such points and optionally a spline or line
    (open or closed).

    Input object can be a image file name or a 3D mesh.
    """
    def __init__(self, obj, initpoints=(), **kwargs):

        super().__init__(**kwargs)

        self.mode    = 'trackball'
        self.verbose = True
        self.splined = True
        self.closed  = False
        self.lcolor  = 'yellow4'
        self.lwidth  = 3
        self.pcolor  = 'purple5'
        self.psize   = 10

        self.cpoints = list(initpoints)
        self.vpoints = None
        self.line = None

        if isinstance(obj, str):
            self.object = vedo.Picture(obj, channels=(0,1,2))
            # keep rgb but drop alpha channel
            self.mode = 'image'
        else:
            self.object = obj

        t = """Click to add a point
        Right-click to remove it
        Drag mouse to change constrast
        Press c to clear points
        Press q to continue""".replace("  ","")
        self.instructions = Text2D(t, pos='bottom-left', c='white', bg='green', font='Calco')

        self.callid1 = self.addCallback('KeyPress', self._keyPress)
        self.callid2 = self.addCallback('LeftButtonPress', self._onLeftClick)
        self.callid3 = self.addCallback('RightButtonPress', self._onRightClick)

    def points(self, newpts=None):
        """Retrieve the 3D coordinates of the clicked points"""
        if newpts is not None:
            self.cpoints = newpts
            self._update()
            return self
        else:
            return np.array(self.cpoints)

    def _onLeftClick(self, evt):
        if not evt.actor: return
        p = evt.picked3d
        self.cpoints.append(p)
        self._update()
        if self.verbose:
            vedo.colors.printc("Added point:", precision(p,4), c='g')

    def _onRightClick(self, evt):
        if evt.actor and len(self.cpoints)>0:
            self.cpoints.pop() # pop removes from the list the last pt
            self._update()
            if self.verbose:
                vedo.colors.printc("Deleted last point", c="r")

    def _update(self):
        self.remove(self.line, self.vpoints)  # remove old points and spline
        self.vpoints = Points(self.cpoints).ps(self.psize).c(self.pcolor)
        self.vpoints.pickable(False)  # avoid picking the same point
        minnr = 1
        if self.splined:
            minnr = 2
        if len(self.cpoints) > minnr:
            if self.splined:
                try:
                    self.line = Spline(self.cpoints, closed=self.closed)
                except ValueError:
                    # if clicking too close splining might fail
                    self.cpoints.pop()
                    return
            else:
                self.line = Line(self.cpoints, closed=self.closed)
            self.line.c(self.lcolor).lw(3).pickable(False)
            self.add(self.vpoints, self.line)
        else:
            self.add(self.vpoints)

    def _keyPress(self, evt):
        if evt.keyPressed == 'c':
            self.cpoints = []
            self.remove(self.line, self.vpoints).render()
            if self.verbose:
                vedo.colors.printc("==== Cleared all points ====", c="r", invert=True)

    def start(self):
        """Start the interaction"""
        self.show(self.object, self.instructions, mode=self.mode)
        return self


########################################################################
class Animation(Plotter):
    """
    A ``Plotter`` derived class that allows to animate simultaneously various objects
    by specifying event times and durations of different visual effects.

    Parameters
    ----------
    totalDuration : float
        expand or shrink the total duration of video to this value

    timeResolution : float
        in seconds, save a frame at this rate

    showProgressBar : bool
        whether to show a progress bar or not

    videoFileName : str
        output file name of the video

    videoFPS : int
        desired value of the nr of frames per second

    .. warning:: this is still an experimental feature at the moment.
    """

    def __init__(
        self,
        totalDuration=None,
        timeResolution=0.02,
        showProgressBar=True,
        videoFileName="animation.mp4",
        videoFPS=12,
    ):
        Plotter.__init__(self)
        self.resetcam = True

        self.events = []
        self.timeResolution = timeResolution
        self.totalDuration = totalDuration
        self.showProgressBar = showProgressBar
        self.videoFileName = videoFileName
        self.videoFPS = videoFPS
        self.bookingMode = True
        self._inputvalues = []
        self._performers = []
        self._lastT = None
        self._lastDuration = None
        self._lastActs = None
        self.eps = 0.00001

    def _parse(self, objs, t, duration):
        if t is None:
            if self._lastT:
                t = self._lastT
            else:
                t = 0.0
        if duration is None:
            if self._lastDuration:
                duration = self._lastDuration
            else:
                duration = 0.0
        if objs is None:
            if self._lastActs:
                objs = self._lastActs
            else:
                vedo.logger.error("Need to specify actors!")
                raise RuntimeError

        objs2 = objs

        if isSequence(objs):
            objs2 = objs
        else:
            objs2 = [objs]

        # quantize time steps and duration
        t = int(t / self.timeResolution + 0.5) * self.timeResolution
        nsteps = int(duration / self.timeResolution + 0.5)
        duration = nsteps * self.timeResolution

        rng = np.linspace(t, t + duration, nsteps + 1)

        self._lastT = t
        self._lastDuration = duration
        self._lastActs = objs2

        for a in objs2:
            if a not in self.actors:
                self.actors.append(a)

        return objs2, t, duration, rng

    def switchOn(self, acts=None, t=None):
        """Switch on the input list of meshes."""
        return self.fadeIn(acts, t, 0)

    def switchOff(self, acts=None, t=None):
        """Switch off the input list of meshes."""
        return self.fadeOut(acts, t, 0)

    def fadeIn(self, acts=None, t=None, duration=None):
        """Gradually switch on the input list of meshes by increasing opacity."""
        if self.bookingMode:
            acts, t, duration, rng = self._parse(acts, t, duration)
            for tt in rng:
                alpha = linInterpolate(tt, [t, t + duration], [0, 1])
                self.events.append((tt, self.fadeIn, acts, alpha))
        else:
            for a in self._performers:
                if hasattr(a, "alpha"):
                    if a.alpha() >= self._inputvalues:
                        continue
                    a.alpha(self._inputvalues)
        return self

    def fadeOut(self, acts=None, t=None, duration=None):
        """Gradually switch off the input list of meshes by increasing transparency."""
        if self.bookingMode:
            acts, t, duration, rng = self._parse(acts, t, duration)
            for tt in rng:
                alpha = linInterpolate(tt, [t, t + duration], [1, 0])
                self.events.append((tt, self.fadeOut, acts, alpha))
        else:
            for a in self._performers:
                if a.alpha() <= self._inputvalues:
                    continue
                a.alpha(self._inputvalues)
        return self

    def changeAlphaBetween(self, alpha1, alpha2, acts=None, t=None, duration=None):
        """Gradually change transparency for the input list of meshes."""
        if self.bookingMode:
            acts, t, duration, rng = self._parse(acts, t, duration)
            for tt in rng:
                alpha = linInterpolate(tt, [t, t + duration], [alpha1, alpha2])
                self.events.append((tt, self.fadeOut, acts, alpha))
        else:
            for a in self._performers:
                a.alpha(self._inputvalues)
        return self

    def changeColor(self, c, acts=None, t=None, duration=None):
        """Gradually change color for the input list of meshes."""
        if self.bookingMode:
            acts, t, duration, rng = self._parse(acts, t, duration)

            col2 = getColor(c)
            for tt in rng:
                inputvalues = []
                for a in acts:
                    col1 = a.color()
                    r = linInterpolate(tt, [t, t + duration], [col1[0], col2[0]])
                    g = linInterpolate(tt, [t, t + duration], [col1[1], col2[1]])
                    b = linInterpolate(tt, [t, t + duration], [col1[2], col2[2]])
                    inputvalues.append((r, g, b))
                self.events.append((tt, self.changeColor, acts, inputvalues))
        else:
            for i, a in enumerate(self._performers):
                a.color(self._inputvalues[i])
        return self

    def changeBackColor(self, c, acts=None, t=None, duration=None):
        """Gradually change backface color for the input list of meshes.
        An initial backface color should be set in advance."""
        if self.bookingMode:
            acts, t, duration, rng = self._parse(acts, t, duration)

            col2 = getColor(c)
            for tt in rng:
                inputvalues = []
                for a in acts:
                    if a.GetBackfaceProperty():
                        col1 = a.backColor()
                        r = linInterpolate(tt, [t, t + duration], [col1[0], col2[0]])
                        g = linInterpolate(tt, [t, t + duration], [col1[1], col2[1]])
                        b = linInterpolate(tt, [t, t + duration], [col1[2], col2[2]])
                        inputvalues.append((r, g, b))
                    else:
                        inputvalues.append(None)
                self.events.append((tt, self.changeBackColor, acts, inputvalues))
        else:
            for i, a in enumerate(self._performers):
                a.backColor(self._inputvalues[i])
        return self

    def changeToWireframe(self, acts=None, t=None):
        """Switch representation to wireframe for the input list of meshes at time `t`."""
        if self.bookingMode:
            acts, t, _, _ = self._parse(acts, t, None)
            self.events.append((t, self.changeToWireframe, acts, True))
        else:
            for a in self._performers:
                a.wireframe(self._inputvalues)
        return self

    def changeToSurface(self, acts=None, t=None):
        """Switch representation to surface for the input list of meshes at time `t`."""
        if self.bookingMode:
            acts, t, _, _ = self._parse(acts, t, None)
            self.events.append((t, self.changeToSurface, acts, False))
        else:
            for a in self._performers:
                a.wireframe(self._inputvalues)
        return self

    def changeLineWidth(self, lw, acts=None, t=None, duration=None):
        """Gradually change line width of the mesh edges for the input list of meshes."""
        if self.bookingMode:
            acts, t, duration, rng = self._parse(acts, t, duration)
            for tt in rng:
                inputvalues = []
                for a in acts:
                    newlw = linInterpolate(tt, [t, t + duration], [a.lw(), lw])
                    inputvalues.append(newlw)
                self.events.append((tt, self.changeLineWidth, acts, inputvalues))
        else:
            for i, a in enumerate(self._performers):
                a.lw(self._inputvalues[i])
        return self

    def changeLineColor(self, c, acts=None, t=None, duration=None):
        """Gradually change line color of the mesh edges for the input list of meshes."""
        if self.bookingMode:
            acts, t, duration, rng = self._parse(acts, t, duration)
            col2 = getColor(c)
            for tt in rng:
                inputvalues = []
                for a in acts:
                    col1 = a.lineColor()
                    r = linInterpolate(tt, [t, t + duration], [col1[0], col2[0]])
                    g = linInterpolate(tt, [t, t + duration], [col1[1], col2[1]])
                    b = linInterpolate(tt, [t, t + duration], [col1[2], col2[2]])
                    inputvalues.append((r, g, b))
                self.events.append((tt, self.changeLineColor, acts, inputvalues))
        else:
            for i, a in enumerate(self._performers):
                a.lineColor(self._inputvalues[i])
        return self

    def changeLighting(self, style, acts=None, t=None, duration=None):
        """Gradually change the lighting style for the input list of meshes.

        Allowed styles are: [metallic, plastic, shiny, glossy, default].
        """
        if self.bookingMode:
            acts, t, duration, rng = self._parse(acts, t, duration)

            c = (1,1,0.99)
            if   style=='metallic': pars = [0.1, 0.3, 1.0, 10, c]
            elif style=='plastic' : pars = [0.3, 0.4, 0.3,  5, c]
            elif style=='shiny'   : pars = [0.2, 0.6, 0.8, 50, c]
            elif style=='glossy'  : pars = [0.1, 0.7, 0.9, 90, c]
            elif style=='default' : pars = [0.1, 1.0, 0.05, 5, c]
            else:
                vedo.logger.error(f"Unknown lighting style {style}")

            for tt in rng:
                inputvalues = []
                for a in acts:
                    pr = a.GetProperty()
                    aa = pr.GetAmbient()
                    ad = pr.GetDiffuse()
                    asp = pr.GetSpecular()
                    aspp = pr.GetSpecularPower()
                    naa  = linInterpolate(tt, [t,t+duration], [aa,  pars[0]])
                    nad  = linInterpolate(tt, [t,t+duration], [ad,  pars[1]])
                    nasp = linInterpolate(tt, [t,t+duration], [asp, pars[2]])
                    naspp= linInterpolate(tt, [t,t+duration], [aspp,pars[3]])
                    inputvalues.append((naa, nad, nasp, naspp))
                self.events.append((tt, self.changeLighting, acts, inputvalues))
        else:
            for i, a in enumerate(self._performers):
                pr = a.GetProperty()
                vals = self._inputvalues[i]
                pr.SetAmbient(vals[0])
                pr.SetDiffuse(vals[1])
                pr.SetSpecular(vals[2])
                pr.SetSpecularPower(vals[3])
        return self

    def move(self, act=None, pt=(0, 0, 0), t=None, duration=None, style="linear"):
        """Smoothly change the position of a specific object to a new point in space."""
        if self.bookingMode:
            acts, t, duration, rng = self._parse(act, t, duration)
            if len(acts) != 1:
                vedo.logger.error("in move(), can move only one object.")
            cpos = acts[0].pos()
            pt = np.array(pt)
            dv = (pt - cpos) / len(rng)
            for j, tt in enumerate(rng):
                i = j + 1
                if "quad" in style:
                    x = i / len(rng)
                    y = x * x
                    self.events.append((tt, self.move, acts, cpos + dv * i * y))
                else:
                    self.events.append((tt, self.move, acts, cpos + dv * i))
        else:
            self._performers[0].pos(self._inputvalues)
        return self

    def rotate(self, act=None, axis=(1, 0, 0), angle=0, t=None, duration=None):
        """Smoothly rotate a specific object by a specified angle and axis."""
        if self.bookingMode:
            acts, t, duration, rng = self._parse(act, t, duration)
            if len(acts) != 1:
                vedo.logger.error("in rotate(), can move only one object.")
            for tt in rng:
                ang = angle / len(rng)
                self.events.append((tt, self.rotate, acts, (axis, ang)))
        else:
            ax = self._inputvalues[0]
            if ax == "x":
                self._performers[0].rotateX(self._inputvalues[1])
            elif ax == "y":
                self._performers[0].rotateY(self._inputvalues[1])
            elif ax == "z":
                self._performers[0].rotateZ(self._inputvalues[1])
        return self

    def scale(self, acts=None, factor=1, t=None, duration=None):
        """Smoothly scale a specific object to a specified scale factor."""
        if self.bookingMode:
            acts, t, duration, rng = self._parse(acts, t, duration)
            for tt in rng:
                fac = linInterpolate(tt, [t, t + duration], [1, factor])
                self.events.append((tt, self.scale, acts, fac))
        else:
            for a in self._performers:
                a.scale(self._inputvalues)
        return self

    def meshErode(self, act=None, corner=6, t=None, duration=None):
        """Erode a mesh by removing cells that are close to one of the 8 corners
        of the bounding box.
        """
        if self.bookingMode:
            acts, t, duration, rng = self._parse(act, t, duration)
            if len(acts) != 1:
                vedo.logger.error("in meshErode(), can erode only one object.")
            diag = acts[0].diagonalSize()
            x0, x1, y0, y1, z0, z1 = acts[0].GetBounds()
            corners = [
                (x0, y0, z0),
                (x1, y0, z0),
                (x1, y1, z0),
                (x0, y1, z0),
                (x0, y0, z1),
                (x1, y0, z1),
                (x1, y1, z1),
                (x0, y1, z1),
            ]
            pcl = acts[0].closestPoint(corners[corner])
            dmin = np.linalg.norm(pcl - corners[corner])
            for tt in rng:
                d = linInterpolate(tt, [t, t + duration], [dmin, diag * 1.01])
                if d > 0:
                    ids = acts[0].closestPoint(
                        corners[corner], radius=d, returnPointId=True
                    )
                    if len(ids) <= acts[0].N():
                        self.events.append((tt, self.meshErode, acts, ids))
        # else:
        #     self._performers[0].deletePoints(self._inputvalues)
        return self

    def play(self):
        """Play the internal list of events and save a video."""

        self.events = sorted(self.events, key=lambda x: x[0])
        self.bookingMode = False

        if self.showProgressBar:
            pb = vedo.ProgressBar(0, len(self.events), c="g")

        if self.totalDuration is None:
            self.totalDuration = self.events[-1][0] - self.events[0][0]

        if self.videoFileName:
            vd = vedo.Video(self.videoFileName, fps=self.videoFPS, duration=self.totalDuration)

        ttlast = 0
        for e in self.events:

            tt, action, self._performers, self._inputvalues = e
            action(0, 0)

            dt = tt - ttlast
            if dt > self.eps:
                self.show(interactive=False, resetcam=self.resetcam)
                if self.videoFileName: vd.addFrame()

                if dt > self.timeResolution+self.eps:
                    if self.videoFileName: vd.pause(dt)

            ttlast = tt

            if self.showProgressBar:
                pb.print("t=" + str(int(tt * 100) / 100) + "s,  " + action.__name__)

        self.show(interactive=False, resetcam=self.resetcam)
        if self.videoFileName:
            vd.addFrame()
            vd.close()

        self.show(interactive=True, resetcam=self.resetcam)
        self.bookingMode = True


class Clock(vedo.Assembly):
    """
    Create a clock with current time or user provided time.

    Parameters
    ----------
    h : int
        hours in range [0,23]

    m : int
        minutes in range [0,59]

    s : int
        seconds in range [0,59]

    font : str
        font type

    title : str
        some extra text to show on the clock

    c : str
        color of the numbers


    Example:
        .. code-block:: python

            import time
            from vedo import show
            from vedo.applications import Clock
            clock = Clock()
            plt = show(clock, interactive=False)
            for i in range(10):
                time.sleep(1)
                clock.update()
                plt.render()
            plt.close()
    """
    def __init__(
            self,
            h=None,
            m=None,
            s=None,
            font="Quikhand",
            title="",
            c="k",
        ):
        self.elapsed = 0
        self._start = time.time()

        wd = ""
        if h is None and m is None:
            t = time.localtime()
            h = t.tm_hour
            m = t.tm_min
            s = t.tm_sec
            if not title:
                d = ["Mon","Tue","Wed","Thu","Fri","Sat","Sun"]
                wd = d[t.tm_wday] + " "

        h = int(h) % 24
        m = int(m) % 60
        t = (h*60+m) / 12 / 60

        if not title:
            if h<10:
                h = f"0{h}"
            if m<10:
                m = f"0{m}"
            title = f"{h}:{m}"

        alpha = 2*np.pi*t + np.pi/2
        beta  = 12*2*np.pi*t + np.pi/2

        x1,y1 = np.cos(alpha), np.sin(alpha)
        x2,y2 = np.cos(beta),  np.sin(beta)
        if s is not None:
            s = int(s) % 60
            gamma = s*2*np.pi /60 + np.pi/2
            x3,y3 = np.cos(gamma), np.sin(gamma)

        ore = Line([0,0], [x1,y1], lw=14, c='red4').scale(0.5).mirror()
        minu= Line([0,0], [x2,y2], lw=7, c='blue3').scale(0.75).mirror()
        secs = None
        if s is not None:
            secs= Line([0,0], [x3,y3], lw=1, c='k').scale(0.95).mirror()
            secs.z(0.003)
        back1 = vedo.shapes.Circle(res=180, c="k6").lw(1)
        back2 = vedo.shapes.Circle(res=12).mirror().scale(0.86).rotateZ(-360/12)
        labels = back2.labels(
            range(1,13),
            justify="center",
            font=font,
            c=c,
        )
        txt = vedo.shapes.Text3D(wd+title,
            font="VictorMono",
            justify="top-center",
            s=.1,
            c=c,
        )
        txt.pos(0,-0.25, 0.001)
        labels.z(0.001)
        minu.z(0.002)
        vedo.Assembly.__init__(self, [back1, labels, ore, minu, secs, txt])
        self.name = "Clock"

    def update(self, h=None, m=None, s=None):
        """Update clock with current or user time"""
        parts = self.unpack()
        self.elapsed = time.time() - self._start

        if h is None and m is None:
            t = time.localtime()
            h = t.tm_hour
            m = t.tm_min
            s = t.tm_sec

        h = int(h) % 24
        m = int(m) % 60
        t = (h*60+m) / 12 / 60

        alpha = 2*np.pi*t + np.pi/2
        beta  = 12*2*np.pi*t + np.pi/2

        x1,y1 = np.cos(alpha), np.sin(alpha)
        x2,y2 = np.cos(beta),  np.sin(beta)
        if s is not None:
            s = int(s) % 60
            gamma = s*2*np.pi /60 + np.pi/2
            x3,y3 = np.cos(gamma), np.sin(gamma)

        pts2 = parts[2].points()
        pts2[1] = [-x1*0.5,y1*0.5, 0.001]
        parts[2].points(pts2)

        pts3 = parts[3].points()
        pts3[1] = [-x2*0.75,y2*0.75, 0.002]
        parts[3].points(pts3)

        if s is not None:
            pts4 = parts[4].points()
            pts4[1] = [-x3*0.95,y3*0.95, 0.003]
            parts[4].points(pts4)

        return self



