vedo.shapes

Submodule to generate simple and complex geometric shapes

   1#!/usr/bin/env python3
   2# -*- coding: utf-8 -*-
   3import os
   4from typing import List, Union, Any
   5from functools import lru_cache
   6from weakref import ref as weak_ref_to
   7
   8import numpy as np
   9import vedo.vtkclasses as vtki
  10
  11import vedo
  12from vedo import settings
  13from vedo.transformations import LinearTransform, pol2cart, cart2spher, spher2cart
  14from vedo.colors import cmaps_names, get_color, printc
  15from vedo import utils
  16from vedo.pointcloud import Points, merge
  17from vedo.mesh import Mesh
  18from vedo.image import Image
  19
  20__docformat__ = "google"
  21
  22__doc__ = """
  23Submodule to generate simple and complex geometric shapes
  24
  25![](https://vedo.embl.es/images/basic/extrude.png)
  26"""
  27
  28__all__ = [
  29    "Marker",
  30    "Line",
  31    "DashedLine",
  32    "RoundedLine",
  33    "Tube",
  34    "Tubes",
  35    "ThickTube",
  36    "Lines",
  37    "Spline",
  38    "KSpline",
  39    "CSpline",
  40    "Bezier",
  41    "Brace",
  42    "NormalLines",
  43    "Ribbon",
  44    "Arrow",
  45    "Arrows",
  46    "Arrow2D",
  47    "Arrows2D",
  48    "FlatArrow",
  49    "Polygon",
  50    "Triangle",
  51    "Rectangle",
  52    "Disc",
  53    "Circle",
  54    "GeoCircle",
  55    "Arc",
  56    "Star",
  57    "Star3D",
  58    "Cross3D",
  59    "IcoSphere",
  60    "Sphere",
  61    "Spheres",
  62    "Earth",
  63    "Ellipsoid",
  64    "Grid",
  65    "TessellatedBox",
  66    "Plane",
  67    "Box",
  68    "Cube",
  69    "Spring",
  70    "Cylinder",
  71    "Cone",
  72    "Pyramid",
  73    "Torus",
  74    "Paraboloid",
  75    "Hyperboloid",
  76    "TextBase",
  77    "Text3D",
  78    "Text2D",
  79    "CornerAnnotation",
  80    "Latex",
  81    "Glyph",
  82    "Tensors",
  83    "ParametricShape",
  84    "ConvexHull",
  85    "VedoLogo",
  86]
  87
  88##############################################
  89_reps = (
  90    (":nabla", "∇"),
  91    (":inf", "∞"),
  92    (":rightarrow", "→"),
  93    (":leftarrow", "←"),
  94    (":partial", "∂"),
  95    (":sqrt", "√"),
  96    (":approx", "≈"),
  97    (":neq", "≠"),
  98    (":leq", "≤"),
  99    (":geq", "≥"),
 100    (":foreach", "∀"),
 101    (":permille", "‰"),
 102    (":euro", "€"),
 103    (":dot", "·"),
 104    (":int", "∫"),
 105    (":pm", "±"),
 106    (":times", "×"),
 107    (":Gamma", "Γ"),
 108    (":Delta", "Δ"),
 109    (":Theta", "Θ"),
 110    (":Lambda", "Λ"),
 111    (":Pi", "Π"),
 112    (":Sigma", "Σ"),
 113    (":Phi", "Φ"),
 114    (":Chi", "X"),
 115    (":Xi", "Ξ"),
 116    (":Psi", "Ψ"),
 117    (":Omega", "Ω"),
 118    (":alpha", "α"),
 119    (":beta", "β"),
 120    (":gamma", "γ"),
 121    (":delta", "δ"),
 122    (":epsilon", "ε"),
 123    (":zeta", "ζ"),
 124    (":eta", "η"),
 125    (":theta", "θ"),
 126    (":kappa", "κ"),
 127    (":lambda", "λ"),
 128    (":mu", "μ"),
 129    (":lowerxi", "ξ"),
 130    (":nu", "ν"),
 131    (":pi", "π"),
 132    (":rho", "ρ"),
 133    (":sigma", "σ"),
 134    (":tau", "τ"),
 135    (":varphi", "φ"),
 136    (":phi", "φ"),
 137    (":chi", "χ"),
 138    (":psi", "ψ"),
 139    (":omega", "ω"),
 140    (":circ", "°"),
 141    (":onehalf", "½"),
 142    (":onefourth", "¼"),
 143    (":threefourths", "¾"),
 144    (":^1", "¹"),
 145    (":^2", "²"),
 146    (":^3", "³"),
 147    (":,", "~"),
 148)
 149
 150
 151########################################################################
 152class Glyph(Mesh):
 153    """
 154    At each vertex of a mesh, another mesh, i.e. a "glyph", is shown with
 155    various orientation options and coloring.
 156
 157    The input can also be a simple list of 2D or 3D coordinates.
 158    Color can be specified as a colormap which maps the size of the orientation
 159    vectors in `orientation_array`.
 160    """
 161
 162    def __init__(
 163        self,
 164        mesh,
 165        glyph,
 166        orientation_array=None,
 167        scale_by_scalar=False,
 168        scale_by_vector_size=False,
 169        scale_by_vector_components=False,
 170        color_by_scalar=False,
 171        color_by_vector_size=False,
 172        c="k8",
 173        alpha=1.0,
 174    ) -> None:
 175        """
 176        Arguments:
 177            orientation_array: (list, str, vtkArray)
 178                list of vectors, `vtkArray` or name of an already existing pointdata array
 179            scale_by_scalar : (bool)
 180                glyph mesh is scaled by the active scalars
 181            scale_by_vector_size : (bool)
 182                glyph mesh is scaled by the size of the vectors
 183            scale_by_vector_components : (bool)
 184                glyph mesh is scaled by the 3 vectors components
 185            color_by_scalar : (bool)
 186                glyph mesh is colored based on the scalar value
 187            color_by_vector_size : (bool)
 188                glyph mesh is colored based on the vector size
 189
 190        Examples:
 191            - [glyphs1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/glyphs1.py)
 192            - [glyphs2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/glyphs2.py)
 193
 194            ![](https://vedo.embl.es/images/basic/glyphs.png)
 195        """
 196        if utils.is_sequence(mesh):
 197            # create a cloud of points
 198            poly = utils.buildPolyData(mesh)
 199        else:
 200            poly = mesh.dataset
 201
 202        cmap = ""
 203        if isinstance(c, str) and c in cmaps_names:
 204            cmap = c
 205            c = None
 206        elif utils.is_sequence(c):  # user passing an array of point colors
 207            ucols = vtki.vtkUnsignedCharArray()
 208            ucols.SetNumberOfComponents(3)
 209            ucols.SetName("GlyphRGB")
 210            for col in c:
 211                cl = get_color(col)
 212                ucols.InsertNextTuple3(cl[0] * 255, cl[1] * 255, cl[2] * 255)
 213            poly.GetPointData().AddArray(ucols)
 214            poly.GetPointData().SetActiveScalars("GlyphRGB")
 215            c = None
 216
 217        gly = vtki.vtkGlyph3D()
 218        gly.GeneratePointIdsOn()
 219        gly.SetInputData(poly)
 220        try:
 221            gly.SetSourceData(glyph)
 222        except TypeError:
 223            gly.SetSourceData(glyph.dataset)
 224
 225        if scale_by_scalar:
 226            gly.SetScaleModeToScaleByScalar()
 227        elif scale_by_vector_size:
 228            gly.SetScaleModeToScaleByVector()
 229        elif scale_by_vector_components:
 230            gly.SetScaleModeToScaleByVectorComponents()
 231        else:
 232            gly.SetScaleModeToDataScalingOff()
 233
 234        if color_by_vector_size:
 235            gly.SetVectorModeToUseVector()
 236            gly.SetColorModeToColorByVector()
 237        elif color_by_scalar:
 238            gly.SetColorModeToColorByScalar()
 239        else:
 240            gly.SetColorModeToColorByScale()
 241
 242        if orientation_array is not None:
 243            gly.OrientOn()
 244            if isinstance(orientation_array, str):
 245                if orientation_array.lower() == "normals":
 246                    gly.SetVectorModeToUseNormal()
 247                else:  # passing a name
 248                    poly.GetPointData().SetActiveVectors(orientation_array)
 249                    gly.SetInputArrayToProcess(0, 0, 0, 0, orientation_array)
 250                    gly.SetVectorModeToUseVector()
 251            elif utils.is_sequence(orientation_array):  # passing a list
 252                varr = vtki.vtkFloatArray()
 253                varr.SetNumberOfComponents(3)
 254                varr.SetName("glyph_vectors")
 255                for v in orientation_array:
 256                    varr.InsertNextTuple(v)
 257                poly.GetPointData().AddArray(varr)
 258                poly.GetPointData().SetActiveVectors("glyph_vectors")
 259                gly.SetInputArrayToProcess(0, 0, 0, 0, "glyph_vectors")
 260                gly.SetVectorModeToUseVector()
 261
 262        gly.Update()
 263
 264        super().__init__(gly.GetOutput(), c, alpha)
 265        self.flat()
 266
 267        if cmap:
 268            self.cmap(cmap, "VectorMagnitude")
 269        elif c is None:
 270            self.pointdata.select("GlyphRGB")
 271
 272        self.name = "Glyph"
 273
 274
 275class Tensors(Mesh):
 276    """
 277    Geometric representation of tensors defined on a domain or set of points.
 278    Tensors can be scaled and/or rotated according to the source at each input point.
 279    Scaling and rotation is controlled by the eigenvalues/eigenvectors of the
 280    symmetrical part of the tensor as follows:
 281
 282    For each tensor, the eigenvalues (and associated eigenvectors) are sorted
 283    to determine the major, medium, and minor eigenvalues/eigenvectors.
 284    The eigenvalue decomposition only makes sense for symmetric tensors,
 285    hence the need to only consider the symmetric part of the tensor,
 286    which is `1/2*(T+T.transposed())`.
 287    """
 288
 289    def __init__(
 290        self,
 291        domain,
 292        source="ellipsoid",
 293        use_eigenvalues=True,
 294        is_symmetric=True,
 295        three_axes=False,
 296        scale=1.0,
 297        max_scale=None,
 298        length=None,
 299        res=24,
 300        c=None,
 301        alpha=1.0,
 302    ) -> None:
 303        """
 304        Arguments:
 305            source : (str, Mesh)
 306                preset types of source shapes is "ellipsoid", "cylinder", "cube" or a `Mesh` object.
 307            use_eigenvalues : (bool)
 308                color source glyph using the eigenvalues or by scalars
 309            three_axes : (bool)
 310                if `False` scale the source in the x-direction,
 311                the medium in the y-direction, and the minor in the z-direction.
 312                Then, the source is rotated so that the glyph's local x-axis lies
 313                along the major eigenvector, y-axis along the medium eigenvector,
 314                and z-axis along the minor.
 315
 316                If `True` three sources are produced, each of them oriented along an eigenvector
 317                and scaled according to the corresponding eigenvector.
 318            is_symmetric : (bool)
 319                If `True` each source glyph is mirrored (2 or 6 glyphs will be produced).
 320                The x-axis of the source glyph will correspond to the eigenvector on output.
 321            length : (float)
 322                distance from the origin to the tip of the source glyph along the x-axis
 323            scale : (float)
 324                scaling factor of the source glyph.
 325            max_scale : (float)
 326                clamp scaling at this factor.
 327
 328        Examples:
 329            - [tensors.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/tensors.py)
 330            - [tensor_grid1.py](https://github.com/marcomusy/vedo/tree/master/examples/other/tensor_grid1.py)
 331            - [tensor_grid2.py](https://github.com/marcomusy/vedo/tree/master/examples/other/tensor_grid2.py)
 332
 333            ![](https://vedo.embl.es/images/volumetric/tensor_grid.png)
 334        """
 335        if isinstance(source, Points):
 336            src = source.dataset
 337        else: # is string
 338            if "ellip" in source:
 339                src = vtki.new("SphereSource")
 340                src.SetPhiResolution(res)
 341                src.SetThetaResolution(res*2)
 342            elif "cyl" in source:
 343                src = vtki.new("CylinderSource")
 344                src.SetResolution(res)
 345                src.CappingOn()
 346            elif source == "cube":
 347                src = vtki.new("CubeSource")
 348            else:
 349                vedo.logger.error(f"Unknown source type {source}")
 350                raise ValueError()
 351            src.Update()
 352            src = src.GetOutput()
 353
 354        tg = vtki.new("TensorGlyph")
 355        if isinstance(domain, vtki.vtkPolyData):
 356            tg.SetInputData(domain)
 357        else:
 358            tg.SetInputData(domain.dataset)
 359        tg.SetSourceData(src)
 360
 361        if c is None:
 362            tg.ColorGlyphsOn()
 363        else:
 364            tg.ColorGlyphsOff()
 365
 366        tg.SetSymmetric(int(is_symmetric))
 367
 368        if length is not None:
 369            tg.SetLength(length)
 370        if use_eigenvalues:
 371            tg.ExtractEigenvaluesOn()
 372            tg.SetColorModeToEigenvalues()
 373        else:
 374            tg.SetColorModeToScalars()
 375
 376        tg.SetThreeGlyphs(three_axes)
 377        tg.ScalingOn()
 378        tg.SetScaleFactor(scale)
 379        if max_scale is None:
 380            tg.ClampScalingOn()
 381            max_scale = scale * 10
 382        tg.SetMaxScaleFactor(max_scale)
 383
 384        tg.Update()
 385        tgn = vtki.new("PolyDataNormals")
 386        tgn.ComputeCellNormalsOff()
 387        tgn.SetInputData(tg.GetOutput())
 388        tgn.Update()
 389
 390        super().__init__(tgn.GetOutput(), c, alpha)
 391        self.name = "Tensors"
 392
 393
 394class Line(Mesh):
 395    """
 396    Build the line segment between point `p0` and point `p1`.
 397
 398    If `p0` is already a list of points, return the line connecting them.
 399
 400    A 2D set of coords can also be passed as `p0=[x..], p1=[y..]`.
 401    """
 402
 403    def __init__(self, p0, p1=None, closed=False, res=2, lw=1, c="k1", alpha=1.0) -> None:
 404        """
 405        Arguments:
 406            closed : (bool)
 407                join last to first point
 408            res : (int)
 409                resolution, number of points along the line
 410                (only relevant if only 2 points are specified)
 411            lw : (int)
 412                line width in pixel units
 413        """
 414
 415        if isinstance(p1, Points):
 416            p1 = p1.pos()
 417            if isinstance(p0, Points):
 418                p0 = p0.pos()
 419        try:
 420            p0 = p0.dataset
 421        except AttributeError:
 422            pass
 423
 424        if isinstance(p0, vtki.vtkPolyData):
 425            poly = p0
 426            top  = np.array([0,0,1])
 427            base = np.array([0,0,0])
 428
 429        elif utils.is_sequence(p0[0]): # detect if user is passing a list of points
 430
 431            p0 = utils.make3d(p0)
 432            ppoints = vtki.vtkPoints()  # Generate the polyline
 433            ppoints.SetData(utils.numpy2vtk(np.asarray(p0), dtype=np.float32))
 434            lines = vtki.vtkCellArray()
 435            npt = len(p0)
 436            if closed:
 437                lines.InsertNextCell(npt + 1)
 438            else:
 439                lines.InsertNextCell(npt)
 440            for i in range(npt):
 441                lines.InsertCellPoint(i)
 442            if closed:
 443                lines.InsertCellPoint(0)
 444            poly = vtki.vtkPolyData()
 445            poly.SetPoints(ppoints)
 446            poly.SetLines(lines)
 447            top = p0[-1]
 448            base = p0[0]
 449            if res != 2:
 450                printc(f"Warning: calling Line(res={res}), try remove []?", c='y')
 451                res = 2
 452
 453        else:  # or just 2 points to link
 454
 455            line_source = vtki.new("LineSource")
 456            p0 = utils.make3d(p0)
 457            p1 = utils.make3d(p1)
 458            line_source.SetPoint1(p0)
 459            line_source.SetPoint2(p1)
 460            line_source.SetResolution(res - 1)
 461            line_source.Update()
 462            poly = line_source.GetOutput()
 463            top = np.asarray(p1, dtype=float)
 464            base = np.asarray(p0, dtype=float)
 465
 466        super().__init__(poly, c, alpha)
 467
 468        self.slope: List[float] = []  # populated by analysis.fit_line
 469        self.center: List[float] = []
 470        self.variances: List[float] = []
 471
 472        self.coefficients: List[float] = []  # populated by pyplot.fit()
 473        self.covariance_matrix: List[float] = []
 474        self.coefficient_errors: List[float] = []
 475        self.monte_carlo_coefficients: List[float] = []
 476        self.reduced_chi2 = -1
 477        self.ndof = 0
 478        self.data_sigma = 0
 479        self.error_lines: List[Any] = []
 480        self.error_band = None
 481        self.res = res
 482
 483        self.lw(lw)
 484        self.properties.LightingOff()
 485        self.actor.PickableOff()
 486        self.actor.DragableOff()
 487        self.base = base
 488        self.top = top
 489        self.name = "Line"
 490
 491    def clone(self, deep=True) -> "Line":
 492        """
 493        Return a copy of the ``Line`` object.
 494
 495        Example:
 496            ```python
 497            from vedo import *
 498            ln1 = Line([1,1,1], [2,2,2], lw=3).print()
 499            ln2 = ln1.clone().shift(0,0,1).c('red').print()
 500            show(ln1, ln2, axes=1, viewup='z').close()
 501            ```
 502            ![](https://vedo.embl.es/images/feats/line_clone.png)
 503        """
 504        poly = vtki.vtkPolyData()
 505        if deep:
 506            poly.DeepCopy(self.dataset)
 507        else:
 508            poly.ShallowCopy(self.dataset)
 509        ln = Line(poly)
 510        ln.copy_properties_from(self)
 511        ln.transform = self.transform.clone()
 512        ln.name = self.name
 513        ln.base = self.base
 514        ln.top = self.top
 515        ln.pipeline = utils.OperationNode(
 516            "clone", parents=[self], shape="diamond", c="#edede9")
 517        return ln
 518
 519    def linecolor(self, lc=None) -> "Line":
 520        """Assign a color to the line"""
 521        # overrides mesh.linecolor which would have no effect here
 522        return self.color(lc)
 523
 524    def eval(self, x: float) -> np.ndarray:
 525        """
 526        Calculate the position of an intermediate point
 527        as a fraction of the length of the line,
 528        being x=0 the first point and x=1 the last point.
 529        This corresponds to an imaginary point that travels along the line
 530        at constant speed.
 531
 532        Can be used in conjunction with `lin_interpolate()`
 533        to map any range to the [0,1] range.
 534        """
 535        distance1 = 0.0
 536        length = self.length()
 537        pts = self.vertices
 538        for i in range(1, len(pts)):
 539            p0 = pts[i - 1]
 540            p1 = pts[i]
 541            seg = p1 - p0
 542            distance0 = distance1
 543            distance1 += np.linalg.norm(seg)
 544            w1 = distance1 / length
 545            if w1 >= x:
 546                break
 547        w0 = distance0 / length
 548        v = p0 + seg * (x - w0) / (w1 - w0)
 549        return v
 550
 551    def find_index_at_position(self, p) -> float:
 552        """
 553        Find the index of the line vertex that is closest to the point `p`.
 554        Note that the returned index can be fractional if `p` is not exactly
 555        one of the vertices of the line.
 556        """
 557        q = self.closest_point(p)
 558        a, b = sorted(self.closest_point(q, n=2, return_point_id=True))
 559        pts = self.vertices
 560        d = np.linalg.norm(pts[a] - pts[b])
 561        t = a + np.linalg.norm(pts[a] - q) / d
 562        return t
 563
 564    def pattern(self, stipple, repeats=10) -> "Line":
 565        """
 566        Define a stipple pattern for dashing the line.
 567        Pass the stipple pattern as a string like `'- - -'`.
 568        Repeats controls the number of times the pattern repeats in a single segment.
 569
 570        Examples are: `'- -', '--  -  --'`, etc.
 571
 572        The resolution of the line (nr of points) can affect how pattern will show up.
 573
 574        Example:
 575            ```python
 576            from vedo import Line
 577            pts = [[1, 0, 0], [5, 2, 0], [3, 3, 1]]
 578            ln = Line(pts, c='r', lw=5).pattern('- -', repeats=10)
 579            ln.show(axes=1).close()
 580            ```
 581            ![](https://vedo.embl.es/images/feats/line_pattern.png)
 582        """
 583        stipple = str(stipple) * int(2 * repeats)
 584        dimension = len(stipple)
 585
 586        image = vtki.vtkImageData()
 587        image.SetDimensions(dimension, 1, 1)
 588        image.AllocateScalars(vtki.VTK_UNSIGNED_CHAR, 4)
 589        image.SetExtent(0, dimension - 1, 0, 0, 0, 0)
 590        i_dim = 0
 591        while i_dim < dimension:
 592            for i in range(dimension):
 593                image.SetScalarComponentFromFloat(i_dim, 0, 0, 0, 255)
 594                image.SetScalarComponentFromFloat(i_dim, 0, 0, 1, 255)
 595                image.SetScalarComponentFromFloat(i_dim, 0, 0, 2, 255)
 596                if stipple[i] == " ":
 597                    image.SetScalarComponentFromFloat(i_dim, 0, 0, 3, 0)
 598                else:
 599                    image.SetScalarComponentFromFloat(i_dim, 0, 0, 3, 255)
 600                i_dim += 1
 601
 602        poly = self.dataset
 603
 604        # Create texture coordinates
 605        tcoords = vtki.vtkDoubleArray()
 606        tcoords.SetName("TCoordsStippledLine")
 607        tcoords.SetNumberOfComponents(1)
 608        tcoords.SetNumberOfTuples(poly.GetNumberOfPoints())
 609        for i in range(poly.GetNumberOfPoints()):
 610            tcoords.SetTypedTuple(i, [i / 2])
 611        poly.GetPointData().SetTCoords(tcoords)
 612        poly.GetPointData().Modified()
 613        texture = vtki.vtkTexture()
 614        texture.SetInputData(image)
 615        texture.InterpolateOff()
 616        texture.RepeatOn()
 617        self.actor.SetTexture(texture)
 618        return self
 619
 620    def length(self) -> float:
 621        """Calculate length of the line."""
 622        distance = 0.0
 623        pts = self.vertices
 624        for i in range(1, len(pts)):
 625            distance += np.linalg.norm(pts[i] - pts[i - 1])
 626        return distance
 627
 628    def tangents(self) -> np.ndarray:
 629        """
 630        Compute the tangents of a line in space.
 631
 632        Example:
 633            ```python
 634            from vedo import *
 635            shape = Assembly(dataurl+"timecourse1d.npy")[58]
 636            pts = shape.rotate_x(30).vertices
 637            tangents = Line(pts).tangents()
 638            arrs = Arrows(pts, pts+tangents, c='blue9')
 639            show(shape.c('red5').lw(5), arrs, bg='bb', axes=1).close()
 640            ```
 641            ![](https://vedo.embl.es/images/feats/line_tangents.png)
 642        """
 643        v = np.gradient(self.vertices)[0]
 644        ds_dt = np.linalg.norm(v, axis=1)
 645        tangent = np.array([1 / ds_dt] * 3).transpose() * v
 646        return tangent
 647
 648    def curvature(self) -> np.ndarray:
 649        """
 650        Compute the signed curvature of a line in space.
 651        The signed is computed assuming the line is about coplanar to the xy plane.
 652
 653        Example:
 654            ```python
 655            from vedo import *
 656            from vedo.pyplot import plot
 657            shape = Assembly(dataurl+"timecourse1d.npy")[55]
 658            curvs = Line(shape.vertices).curvature()
 659            shape.cmap('coolwarm', curvs, vmin=-2,vmax=2).add_scalarbar3d(c='w')
 660            shape.render_lines_as_tubes().lw(12)
 661            pp = plot(curvs, ac='white', lc='yellow5')
 662            show(shape, pp, N=2, bg='bb', sharecam=False).close()
 663            ```
 664            ![](https://vedo.embl.es/images/feats/line_curvature.png)
 665        """
 666        v = np.gradient(self.vertices)[0]
 667        a = np.gradient(v)[0]
 668        av = np.cross(a, v)
 669        mav = np.linalg.norm(av, axis=1)
 670        mv = utils.mag2(v)
 671        val = mav * np.sign(av[:, 2]) / np.power(mv, 1.5)
 672        val[0] = val[1]
 673        val[-1] = val[-2]
 674        return val
 675
 676    def compute_curvature(self, method=0) -> "Line":
 677        """
 678        Add a pointdata array named 'Curvatures' which contains
 679        the curvature value at each point.
 680
 681        NB: keyword `method` is overridden in Mesh and has no effect here.
 682        """
 683        # overrides mesh.compute_curvature
 684        curvs = self.curvature()
 685        vmin, vmax = np.min(curvs), np.max(curvs)
 686        if vmin < 0 and vmax > 0:
 687            v = max(-vmin, vmax)
 688            self.cmap("coolwarm", curvs, vmin=-v, vmax=v, name="Curvature")
 689        else:
 690            self.cmap("coolwarm", curvs, vmin=vmin, vmax=vmax, name="Curvature")
 691        return self
 692
 693    def plot_scalar(
 694            self,
 695            radius=0.0, 
 696            height=1.1,
 697            normal=(),
 698            camera=None,
 699        ) -> "Line":
 700        """
 701        Generate a new `Line` which plots the active scalar along the line.
 702
 703        Arguments:
 704            radius : (float)
 705                distance radius to the line
 706            height: (float)
 707                height of the plot
 708            normal: (list)
 709                normal vector to the plane of the plot
 710            camera: (vtkCamera) 
 711                camera object to use for the plot orientation
 712        
 713        Example:
 714            ```python
 715            from vedo import *
 716            circle = Circle(res=360).rotate_y(20)
 717            pts = circle.vertices
 718            bore = Line(pts).lw(5)
 719            values = np.arctan2(pts[:,1], pts[:,0])
 720            bore.pointdata["scalars"] = values + np.random.randn(360)/5
 721            vap = bore.plot_scalar(radius=0, height=1)
 722            show(bore, vap, axes=1, viewup='z').close()
 723            ```
 724            ![](https://vedo.embl.es/images/feats/line_plot_scalar.png)
 725        """
 726        ap = vtki.new("ArcPlotter")
 727        ap.SetInputData(self.dataset)
 728        ap.SetCamera(camera)
 729        ap.SetRadius(radius)
 730        ap.SetHeight(height)
 731        if len(normal)>0:
 732            ap.UseDefaultNormalOn()
 733            ap.SetDefaultNormal(normal)
 734        ap.Update()
 735        vap = Line(ap.GetOutput())
 736        vap.linewidth(3).lighting('off')
 737        vap.name = "ArcPlot"
 738        return vap
 739
 740    def sweep(self, direction=(1, 0, 0), res=1) -> "Mesh":
 741        """
 742        Sweep the `Line` along the specified vector direction.
 743
 744        Returns a `Mesh` surface.
 745        Line position is updated to allow for additional sweepings.
 746
 747        Example:
 748            ```python
 749            from vedo import Line, show
 750            aline = Line([(0,0,0),(1,3,0),(2,4,0)])
 751            surf1 = aline.sweep((1,0.2,0), res=3)
 752            surf2 = aline.sweep((0.2,0,1)).alpha(0.5)
 753            aline.color('r').linewidth(4)
 754            show(surf1, surf2, aline, axes=1).close()
 755            ```
 756            ![](https://vedo.embl.es/images/feats/sweepline.png)
 757        """
 758        line = self.dataset
 759        rows = line.GetNumberOfPoints()
 760
 761        spacing = 1 / res
 762        surface = vtki.vtkPolyData()
 763
 764        res += 1
 765        npts = rows * res
 766        npolys = (rows - 1) * (res - 1)
 767        points = vtki.vtkPoints()
 768        points.Allocate(npts)
 769
 770        cnt = 0
 771        x = [0.0, 0.0, 0.0]
 772        for row in range(rows):
 773            for col in range(res):
 774                p = [0.0, 0.0, 0.0]
 775                line.GetPoint(row, p)
 776                x[0] = p[0] + direction[0] * col * spacing
 777                x[1] = p[1] + direction[1] * col * spacing
 778                x[2] = p[2] + direction[2] * col * spacing
 779                points.InsertPoint(cnt, x)
 780                cnt += 1
 781
 782        # Generate the quads
 783        polys = vtki.vtkCellArray()
 784        polys.Allocate(npolys * 4)
 785        pts = [0, 0, 0, 0]
 786        for row in range(rows - 1):
 787            for col in range(res - 1):
 788                pts[0] = col + row * res
 789                pts[1] = pts[0] + 1
 790                pts[2] = pts[0] + res + 1
 791                pts[3] = pts[0] + res
 792                polys.InsertNextCell(4, pts)
 793        surface.SetPoints(points)
 794        surface.SetPolys(polys)
 795        asurface = Mesh(surface)
 796        asurface.copy_properties_from(self)
 797        asurface.lighting("default")
 798        self.vertices = self.vertices + direction
 799        return asurface
 800
 801    def reverse(self):
 802        """Reverse the points sequence order."""
 803        pts = np.flip(self.vertices, axis=0)
 804        self.vertices = pts
 805        return self
 806
 807
 808class DashedLine(Mesh):
 809    """
 810    Consider using `Line.pattern()` instead.
 811
 812    Build a dashed line segment between points `p0` and `p1`.
 813    If `p0` is a list of points returns the line connecting them.
 814    A 2D set of coords can also be passed as `p0=[x..], p1=[y..]`.
 815    """
 816
 817    def __init__(self, p0, p1=None, spacing=0.1, closed=False, lw=2, c="k5", alpha=1.0) -> None:
 818        """
 819        Arguments:
 820            closed : (bool)
 821                join last to first point
 822            spacing : (float)
 823                relative size of the dash
 824            lw : (int)
 825                line width in pixels
 826        """
 827        if isinstance(p1, vtki.vtkActor):
 828            p1 = p1.GetPosition()
 829            if isinstance(p0, vtki.vtkActor):
 830                p0 = p0.GetPosition()
 831        if isinstance(p0, Points):
 832            p0 = p0.vertices
 833
 834        # detect if user is passing a 2D list of points as p0=xlist, p1=ylist:
 835        if len(p0) > 3:
 836            if not utils.is_sequence(p0[0]) and not utils.is_sequence(p1[0]) and len(p0) == len(p1):
 837                # assume input is 2D xlist, ylist
 838                p0 = np.stack((p0, p1), axis=1)
 839                p1 = None
 840            p0 = utils.make3d(p0)
 841            if closed:
 842                p0 = np.append(p0, [p0[0]], axis=0)
 843
 844        if p1 is not None:  # assume passing p0=[x,y]
 845            if len(p0) == 2 and not utils.is_sequence(p0[0]):
 846                p0 = (p0[0], p0[1], 0)
 847            if len(p1) == 2 and not utils.is_sequence(p1[0]):
 848                p1 = (p1[0], p1[1], 0)
 849
 850        # detect if user is passing a list of points:
 851        if utils.is_sequence(p0[0]):
 852            listp = p0
 853        else:  # or just 2 points to link
 854            listp = [p0, p1]
 855
 856        listp = np.array(listp)
 857        if listp.shape[1] == 2:
 858            listp = np.c_[listp, np.zeros(listp.shape[0])]
 859
 860        xmn = np.min(listp, axis=0)
 861        xmx = np.max(listp, axis=0)
 862        dlen = np.linalg.norm(xmx - xmn) * np.clip(spacing, 0.01, 1.0) / 10
 863        if not dlen:
 864            super().__init__(vtki.vtkPolyData(), c, alpha)
 865            self.name = "DashedLine (void)"
 866            return
 867
 868        qs = []
 869        for ipt in range(len(listp) - 1):
 870            p0 = listp[ipt]
 871            p1 = listp[ipt + 1]
 872            v = p1 - p0
 873            vdist = np.linalg.norm(v)
 874            n1 = int(vdist / dlen)
 875            if not n1:
 876                continue
 877
 878            res = 0.0
 879            for i in range(n1 + 2):
 880                ist = (i - 0.5) / n1
 881                ist = max(ist, 0)
 882                qi = p0 + v * (ist - res / vdist)
 883                if ist > 1:
 884                    qi = p1
 885                    res = np.linalg.norm(qi - p1)
 886                    qs.append(qi)
 887                    break
 888                qs.append(qi)
 889
 890        polylns = vtki.new("AppendPolyData")
 891        for i, q1 in enumerate(qs):
 892            if not i % 2:
 893                continue
 894            q0 = qs[i - 1]
 895            line_source = vtki.new("LineSource")
 896            line_source.SetPoint1(q0)
 897            line_source.SetPoint2(q1)
 898            line_source.Update()
 899            polylns.AddInputData(line_source.GetOutput())
 900        polylns.Update()
 901
 902        super().__init__(polylns.GetOutput(), c, alpha)
 903        self.lw(lw).lighting("off")
 904        self.base = listp[0]
 905        if closed:
 906            self.top = listp[-2]
 907        else:
 908            self.top = listp[-1]
 909        self.name = "DashedLine"
 910
 911
 912class RoundedLine(Mesh):
 913    """
 914    Create a 2D line of specified thickness (in absolute units) passing through
 915    a list of input points. Borders of the line are rounded.
 916    """
 917
 918    def __init__(self, pts, lw, res=10, c="gray4", alpha=1.0) -> None:
 919        """
 920        Arguments:
 921            pts : (list)
 922                a list of points in 2D or 3D (z will be ignored).
 923            lw : (float)
 924                thickness of the line.
 925            res : (int)
 926                resolution of the rounded regions
 927
 928        Example:
 929            ```python
 930            from vedo import *
 931            pts = [(-4,-3),(1,1),(2,4),(4,1),(3,-1),(2,-5),(9,-3)]
 932            ln = Line(pts).z(0.01)
 933            ln.color("red5").linewidth(2)
 934            rl = RoundedLine(pts, 0.6)
 935            show(Points(pts), ln, rl, axes=1).close()
 936            ```
 937            ![](https://vedo.embl.es/images/feats/rounded_line.png)
 938        """
 939        pts = utils.make3d(pts)
 940
 941        def _getpts(pts, revd=False):
 942
 943            if revd:
 944                pts = list(reversed(pts))
 945
 946            if len(pts) == 2:
 947                p0, p1 = pts
 948                v = p1 - p0
 949                dv = np.linalg.norm(v)
 950                nv = np.cross(v, (0, 0, -1))
 951                nv = nv / np.linalg.norm(nv) * lw
 952                return [p0 + nv, p1 + nv]
 953
 954            ptsnew = []
 955            for k in range(len(pts) - 2):
 956                p0 = pts[k]
 957                p1 = pts[k + 1]
 958                p2 = pts[k + 2]
 959                v = p1 - p0
 960                u = p2 - p1
 961                du = np.linalg.norm(u)
 962                dv = np.linalg.norm(v)
 963                nv = np.cross(v, (0, 0, -1))
 964                nv = nv / np.linalg.norm(nv) * lw
 965                nu = np.cross(u, (0, 0, -1))
 966                nu = nu / np.linalg.norm(nu) * lw
 967                uv = np.cross(u, v)
 968                if k == 0:
 969                    ptsnew.append(p0 + nv)
 970                if uv[2] <= 0:
 971                    # the following computation can return a value
 972                    # ever so slightly > 1.0 causing arccos to fail.
 973                    uv_arg = np.dot(u, v) / du / dv
 974                    if uv_arg > 1.0:
 975                        # since the argument to arcos is 1, simply
 976                        # assign alpha to 0.0 without calculating the
 977                        # arccos
 978                        alpha = 0.0
 979                    else:
 980                        alpha = np.arccos(uv_arg)
 981                    db = lw * np.tan(alpha / 2)
 982                    p1new = p1 + nv - v / dv * db
 983                    ptsnew.append(p1new)
 984                else:
 985                    p1a = p1 + nv
 986                    p1b = p1 + nu
 987                    for i in range(0, res + 1):
 988                        pab = p1a * (res - i) / res + p1b * i / res
 989                        vpab = pab - p1
 990                        vpab = vpab / np.linalg.norm(vpab) * lw
 991                        ptsnew.append(p1 + vpab)
 992                if k == len(pts) - 3:
 993                    ptsnew.append(p2 + nu)
 994                    if revd:
 995                        ptsnew.append(p2 - nu)
 996            return ptsnew
 997
 998        ptsnew = _getpts(pts) + _getpts(pts, revd=True)
 999
1000        ppoints = vtki.vtkPoints()  # Generate the polyline
1001        ppoints.SetData(utils.numpy2vtk(np.asarray(ptsnew), dtype=np.float32))
1002        lines = vtki.vtkCellArray()
1003        npt = len(ptsnew)
1004        lines.InsertNextCell(npt)
1005        for i in range(npt):
1006            lines.InsertCellPoint(i)
1007        poly = vtki.vtkPolyData()
1008        poly.SetPoints(ppoints)
1009        poly.SetLines(lines)
1010        vct = vtki.new("ContourTriangulator")
1011        vct.SetInputData(poly)
1012        vct.Update()
1013
1014        super().__init__(vct.GetOutput(), c, alpha)
1015        self.flat()
1016        self.properties.LightingOff()
1017        self.name = "RoundedLine"
1018        self.base = ptsnew[0]
1019        self.top = ptsnew[-1]
1020
1021
1022class Lines(Mesh):
1023    """
1024    Build the line segments between two lists of points `start_pts` and `end_pts`.
1025    `start_pts` can be also passed in the form `[[point1, point2], ...]`.
1026    """
1027
1028    def __init__(
1029        self, start_pts, end_pts=None, dotted=False, res=1, scale=1.0, lw=1, c="k4", alpha=1.0
1030    ) -> None:
1031        """
1032        Arguments:
1033            scale : (float)
1034                apply a rescaling factor to the lengths.
1035            c : (color, int, str, list)
1036                color name, number, or list of [R,G,B] colors
1037            alpha : (float)
1038                opacity in range [0,1]
1039            lw : (int)
1040                line width in pixel units
1041            dotted : (bool)
1042                draw a dotted line
1043            res : (int)
1044                resolution, number of points along the line
1045                (only relevant if only 2 points are specified)
1046
1047        Examples:
1048            - [fitspheres2.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/fitspheres2.py)
1049
1050            ![](https://user-images.githubusercontent.com/32848391/52503049-ac9cb600-2be4-11e9-86af-72a538af14ef.png)
1051        """
1052
1053        if isinstance(start_pts, vtki.vtkPolyData):########
1054            super().__init__(start_pts, c, alpha)
1055            self.lw(lw).lighting("off")
1056            self.name = "Lines"
1057            return ########################################
1058
1059        if utils.is_sequence(start_pts) and len(start_pts)>1 and isinstance(start_pts[0], Line):
1060            # passing a list of Line, see tests/issues/issue_950.py
1061            polylns = vtki.new("AppendPolyData")
1062            for ln in start_pts:
1063                polylns.AddInputData(ln.dataset)
1064            polylns.Update()
1065
1066            super().__init__(polylns.GetOutput(), c, alpha)
1067            self.lw(lw).lighting("off")
1068            if dotted:
1069                self.properties.SetLineStipplePattern(0xF0F0)
1070                self.properties.SetLineStippleRepeatFactor(1)
1071            self.name = "Lines"
1072            return ########################################
1073
1074        if isinstance(start_pts, Points):
1075            start_pts = start_pts.vertices
1076        if isinstance(end_pts, Points):
1077            end_pts = end_pts.vertices
1078
1079        if end_pts is not None:
1080            start_pts = np.stack((start_pts, end_pts), axis=1)
1081
1082        polylns = vtki.new("AppendPolyData")
1083
1084        if not utils.is_ragged(start_pts):
1085
1086            for twopts in start_pts:
1087                line_source = vtki.new("LineSource")
1088                line_source.SetResolution(res)
1089                if len(twopts[0]) == 2:
1090                    line_source.SetPoint1(twopts[0][0], twopts[0][1], 0.0)
1091                else:
1092                    line_source.SetPoint1(twopts[0])
1093
1094                if scale == 1:
1095                    pt2 = twopts[1]
1096                else:
1097                    vers = (np.array(twopts[1]) - twopts[0]) * scale
1098                    pt2 = np.array(twopts[0]) + vers
1099
1100                if len(pt2) == 2:
1101                    line_source.SetPoint2(pt2[0], pt2[1], 0.0)
1102                else:
1103                    line_source.SetPoint2(pt2)
1104                polylns.AddInputConnection(line_source.GetOutputPort())
1105
1106        else:
1107
1108            polylns = vtki.new("AppendPolyData")
1109            for t in start_pts:
1110                t = utils.make3d(t)
1111                ppoints = vtki.vtkPoints()  # Generate the polyline
1112                ppoints.SetData(utils.numpy2vtk(t, dtype=np.float32))
1113                lines = vtki.vtkCellArray()
1114                npt = len(t)
1115                lines.InsertNextCell(npt)
1116                for i in range(npt):
1117                    lines.InsertCellPoint(i)
1118                poly = vtki.vtkPolyData()
1119                poly.SetPoints(ppoints)
1120                poly.SetLines(lines)
1121                polylns.AddInputData(poly)
1122
1123        polylns.Update()
1124
1125        super().__init__(polylns.GetOutput(), c, alpha)
1126        self.lw(lw).lighting("off")
1127        if dotted:
1128            self.properties.SetLineStipplePattern(0xF0F0)
1129            self.properties.SetLineStippleRepeatFactor(1)
1130
1131        self.name = "Lines"
1132
1133
1134class Spline(Line):
1135    """
1136    Find the B-Spline curve through a set of points. This curve does not necessarily
1137    pass exactly through all the input points. Needs to import `scipy`.
1138    """
1139
1140    def __init__(self, points, smooth=0.0, degree=2, closed=False, res=None, easing="") -> None:
1141        """
1142        Arguments:
1143            smooth : (float)
1144                smoothing factor.
1145                - 0 = interpolate points exactly [default].
1146                - 1 = average point positions.
1147            degree : (int)
1148                degree of the spline (between 1 and 5).
1149            easing : (str)
1150                control sensity of points along the spline.
1151                Available options are
1152                `[InSine, OutSine, Sine, InQuad, OutQuad, InCubic, OutCubic, InQuart, OutQuart, InCirc, OutCirc].`
1153                Can be used to create animations (move objects at varying speed).
1154                See e.g.: https://easings.net
1155            res : (int)
1156                number of points on the spline
1157
1158        See also: `CSpline` and `KSpline`.
1159
1160        Examples:
1161            - [spline_ease.py](https://github.com/marcomusy/vedo/tree/master/examples/simulations/spline_ease.py)
1162
1163                ![](https://vedo.embl.es/images/simulations/spline_ease.gif)
1164        """
1165        from scipy.interpolate import splprep, splev
1166
1167        if isinstance(points, Points):
1168            points = points.vertices
1169
1170        points = utils.make3d(points)
1171
1172        per = 0
1173        if closed:
1174            points = np.append(points, [points[0]], axis=0)
1175            per = 1
1176
1177        if res is None:
1178            res = len(points) * 10
1179
1180        points = np.array(points, dtype=float)
1181
1182        minx, miny, minz = np.min(points, axis=0)
1183        maxx, maxy, maxz = np.max(points, axis=0)
1184        maxb = max(maxx - minx, maxy - miny, maxz - minz)
1185        smooth *= maxb / 2  # must be in absolute units
1186
1187        x = np.linspace(0.0, 1.0, res)
1188        if easing:
1189            if easing == "InSine":
1190                x = 1.0 - np.cos((x * np.pi) / 2)
1191            elif easing == "OutSine":
1192                x = np.sin((x * np.pi) / 2)
1193            elif easing == "Sine":
1194                x = -(np.cos(np.pi * x) - 1) / 2
1195            elif easing == "InQuad":
1196                x = x * x
1197            elif easing == "OutQuad":
1198                x = 1.0 - (1 - x) * (1 - x)
1199            elif easing == "InCubic":
1200                x = x * x
1201            elif easing == "OutCubic":
1202                x = 1.0 - np.power(1 - x, 3)
1203            elif easing == "InQuart":
1204                x = x * x * x * x
1205            elif easing == "OutQuart":
1206                x = 1.0 - np.power(1 - x, 4)
1207            elif easing == "InCirc":
1208                x = 1.0 - np.sqrt(1 - np.power(x, 2))
1209            elif easing == "OutCirc":
1210                x = np.sqrt(1.0 - np.power(x - 1, 2))
1211            else:
1212                vedo.logger.error(f"unknown ease mode {easing}")
1213
1214        # find the knots
1215        tckp, _ = splprep(points.T, task=0, s=smooth, k=degree, per=per)
1216        # evaluate spLine, including interpolated points:
1217        xnew, ynew, znew = splev(x, tckp)
1218
1219        super().__init__(np.c_[xnew, ynew, znew], lw=2)
1220        self.name = "Spline"
1221
1222
1223class KSpline(Line):
1224    """
1225    Return a [Kochanek spline](https://en.wikipedia.org/wiki/Kochanek%E2%80%93Bartels_spline)
1226    which runs exactly through all the input points.
1227    """
1228
1229    def __init__(self, points, 
1230                 continuity=0.0, tension=0.0, bias=0.0, closed=False, res=None) -> None:
1231        """
1232        Arguments:
1233            continuity : (float)
1234                changes the sharpness in change between tangents
1235            tension : (float)
1236                changes the length of the tangent vector
1237            bias : (float)
1238                changes the direction of the tangent vector
1239            closed : (bool)
1240                join last to first point to produce a closed curve
1241            res : (int)
1242                approximate resolution of the output line.
1243                Default is 20 times the number of input points.
1244
1245        ![](https://user-images.githubusercontent.com/32848391/65975805-73fd6580-e46f-11e9-8957-75eddb28fa72.png)
1246
1247        Warning:
1248            This class is not necessarily generating the exact number of points
1249            as requested by `res`. Some points may be concident and removed.
1250
1251        See also: `Spline` and `CSpline`.
1252        """
1253        if isinstance(points, Points):
1254            points = points.vertices
1255
1256        if not res:
1257            res = len(points) * 20
1258
1259        points = utils.make3d(points).astype(float)
1260
1261        vtkKochanekSpline = vtki.get_class("KochanekSpline")
1262        xspline = vtkKochanekSpline()
1263        yspline = vtkKochanekSpline()
1264        zspline = vtkKochanekSpline()
1265        for s in [xspline, yspline, zspline]:
1266            if bias:
1267                s.SetDefaultBias(bias)
1268            if tension:
1269                s.SetDefaultTension(tension)
1270            if continuity:
1271                s.SetDefaultContinuity(continuity)
1272            s.SetClosed(closed)
1273
1274        lenp = len(points[0]) > 2
1275
1276        for i, p in enumerate(points):
1277            xspline.AddPoint(i, p[0])
1278            yspline.AddPoint(i, p[1])
1279            if lenp:
1280                zspline.AddPoint(i, p[2])
1281
1282        ln = []
1283        for pos in np.linspace(0, len(points), res):
1284            x = xspline.Evaluate(pos)
1285            y = yspline.Evaluate(pos)
1286            z = 0
1287            if lenp:
1288                z = zspline.Evaluate(pos)
1289            ln.append((x, y, z))
1290
1291        super().__init__(ln, lw=2)
1292        self.clean()
1293        self.lighting("off")
1294        self.name = "KSpline"
1295        self.base = np.array(points[0], dtype=float)
1296        self.top = np.array(points[-1], dtype=float)
1297
1298
1299class CSpline(Line):
1300    """
1301    Return a Cardinal spline which runs exactly through all the input points.
1302    """
1303
1304    def __init__(self, points, closed=False, res=None) -> None:
1305        """
1306        Arguments:
1307            closed : (bool)
1308                join last to first point to produce a closed curve
1309            res : (int)
1310                approximate resolution of the output line.
1311                Default is 20 times the number of input points.
1312
1313        Warning:
1314            This class is not necessarily generating the exact number of points
1315            as requested by `res`. Some points may be concident and removed.
1316
1317        See also: `Spline` and `KSpline`.
1318        """
1319
1320        if isinstance(points, Points):
1321            points = points.vertices
1322
1323        if not res:
1324            res = len(points) * 20
1325
1326        points = utils.make3d(points).astype(float)
1327
1328        vtkCardinalSpline = vtki.get_class("CardinalSpline")
1329        xspline = vtkCardinalSpline()
1330        yspline = vtkCardinalSpline()
1331        zspline = vtkCardinalSpline()
1332        for s in [xspline, yspline, zspline]:
1333            s.SetClosed(closed)
1334
1335        lenp = len(points[0]) > 2
1336
1337        for i, p in enumerate(points):
1338            xspline.AddPoint(i, p[0])
1339            yspline.AddPoint(i, p[1])
1340            if lenp:
1341                zspline.AddPoint(i, p[2])
1342
1343        ln = []
1344        for pos in np.linspace(0, len(points), res):
1345            x = xspline.Evaluate(pos)
1346            y = yspline.Evaluate(pos)
1347            z = 0
1348            if lenp:
1349                z = zspline.Evaluate(pos)
1350            ln.append((x, y, z))
1351
1352        super().__init__(ln, lw=2)
1353        self.clean()
1354        self.lighting("off")
1355        self.name = "CSpline"
1356        self.base = points[0]
1357        self.top = points[-1]
1358
1359
1360class Bezier(Line):
1361    """
1362    Generate the Bezier line that links the first to the last point.
1363    """
1364
1365    def __init__(self, points, res=None) -> None:
1366        """
1367        Example:
1368            ```python
1369            from vedo import *
1370            import numpy as np
1371            pts = np.random.randn(25,3)
1372            for i,p in enumerate(pts):
1373                p += [5*i, 15*sin(i/2), i*i*i/200]
1374            show(Points(pts), Bezier(pts), axes=1).close()
1375            ```
1376            ![](https://user-images.githubusercontent.com/32848391/90437534-dafd2a80-e0d2-11ea-9b93-9ecb3f48a3ff.png)
1377        """
1378        N = len(points)
1379        if res is None:
1380            res = 10 * N
1381        t = np.linspace(0, 1, num=res)
1382        bcurve = np.zeros((res, len(points[0])))
1383
1384        def binom(n, k):
1385            b = 1
1386            for t in range(1, min(k, n - k) + 1):
1387                b *= n / t
1388                n -= 1
1389            return b
1390
1391        def bernstein(n, k):
1392            coeff = binom(n, k)
1393
1394            def _bpoly(x):
1395                return coeff * x ** k * (1 - x) ** (n - k)
1396
1397            return _bpoly
1398
1399        for ii in range(N):
1400            b = bernstein(N - 1, ii)(t)
1401            bcurve += np.outer(b, points[ii])
1402        super().__init__(bcurve, lw=2)
1403        self.name = "BezierLine"
1404
1405
1406class NormalLines(Mesh):
1407    """
1408    Build an `Glyph` to show the normals at cell centers or at mesh vertices.
1409
1410    Arguments:
1411        ratio : (int)
1412            show 1 normal every `ratio` cells.
1413        on : (str)
1414            either "cells" or "points".
1415        scale : (float)
1416            scale factor to control size.
1417    """
1418
1419    def __init__(self, msh, ratio=1, on="cells", scale=1.0) -> None:
1420
1421        poly = msh.clone().dataset
1422
1423        if "cell" in on:
1424            centers = vtki.new("CellCenters")
1425            centers.SetInputData(poly)
1426            centers.Update()
1427            poly = centers.GetOutput()
1428
1429        mask_pts = vtki.new("MaskPoints")
1430        mask_pts.SetInputData(poly)
1431        mask_pts.SetOnRatio(ratio)
1432        mask_pts.RandomModeOff()
1433        mask_pts.Update()
1434
1435        ln = vtki.new("LineSource")
1436        ln.SetPoint1(0, 0, 0)
1437        ln.SetPoint2(1, 0, 0)
1438        ln.Update()
1439        glyph = vtki.vtkGlyph3D()
1440        glyph.SetSourceData(ln.GetOutput())
1441        glyph.SetInputData(mask_pts.GetOutput())
1442        glyph.SetVectorModeToUseNormal()
1443
1444        b = poly.GetBounds()
1445        f = max([b[1] - b[0], b[3] - b[2], b[5] - b[4]]) / 50 * scale
1446        glyph.SetScaleFactor(f)
1447        glyph.OrientOn()
1448        glyph.Update()
1449
1450        super().__init__(glyph.GetOutput())
1451
1452        self.actor.PickableOff()
1453        prop = vtki.vtkProperty()
1454        prop.DeepCopy(msh.properties)
1455        self.actor.SetProperty(prop)
1456        self.properties = prop
1457        self.properties.LightingOff()
1458        self.mapper.ScalarVisibilityOff()
1459        self.name = "NormalLines"
1460
1461
1462class Tube(Mesh):
1463    """
1464    Build a tube along the line defined by a set of points.
1465    """
1466
1467    def __init__(self, points, r=1.0, cap=True, res=12, c=None, alpha=1.0) -> None:
1468        """
1469        Arguments:
1470            r :  (float, list)
1471                constant radius or list of radii.
1472            res : (int)
1473                resolution, number of the sides of the tube
1474            c : (color)
1475                constant color or list of colors for each point.
1476            
1477        Example:
1478            Create a tube along a line, with data associated to each point:
1479
1480            ```python
1481            from vedo import *
1482            line = Line([(0,0,0), (1,1,1), (2,0,1), (3,1,0)]).lw(5)
1483            scalars = np.array([0, 1, 2, 3])
1484            line.pointdata["myscalars"] = scalars
1485            tube = Tube(line, r=0.1).lw(1)
1486            tube.cmap('viridis', "myscalars").add_scalarbar3d()
1487            show(line, tube, axes=1).close()
1488            ```
1489
1490        Examples:
1491            - [ribbon.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/ribbon.py)
1492            - [tube_radii.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/tube_radii.py)
1493
1494                ![](https://vedo.embl.es/images/basic/tube.png)
1495        """
1496        if utils.is_sequence(points):
1497            vpoints = vtki.vtkPoints()
1498            idx = len(points)
1499            for p in points:
1500                vpoints.InsertNextPoint(p)
1501            line = vtki.new("PolyLine")
1502            line.GetPointIds().SetNumberOfIds(idx)
1503            for i in range(idx):
1504                line.GetPointIds().SetId(i, i)
1505            lines = vtki.vtkCellArray()
1506            lines.InsertNextCell(line)
1507            polyln = vtki.vtkPolyData()
1508            polyln.SetPoints(vpoints)
1509            polyln.SetLines(lines)            
1510            self.base = np.asarray(points[0], dtype=float)
1511            self.top = np.asarray(points[-1], dtype=float)
1512
1513        elif isinstance(points, Mesh):
1514            polyln = points.dataset
1515            n = polyln.GetNumberOfPoints()
1516            self.base = np.array(polyln.GetPoint(0))
1517            self.top = np.array(polyln.GetPoint(n - 1))
1518
1519        # from vtkmodules.vtkFiltersCore import vtkTubeBender
1520        # bender = vtkTubeBender()
1521        # bender.SetInputData(polyln)
1522        # bender.SetRadius(r)
1523        # bender.Update()
1524        # polyln = bender.GetOutput()
1525
1526        tuf = vtki.new("TubeFilter")
1527        tuf.SetCapping(cap)
1528        tuf.SetNumberOfSides(res)
1529        tuf.SetInputData(polyln)
1530        if utils.is_sequence(r):
1531            arr = utils.numpy2vtk(r, dtype=float)
1532            arr.SetName("TubeRadius")
1533            polyln.GetPointData().AddArray(arr)
1534            polyln.GetPointData().SetActiveScalars("TubeRadius")
1535            tuf.SetVaryRadiusToVaryRadiusByAbsoluteScalar()
1536        else:
1537            tuf.SetRadius(r)
1538
1539        usingColScals = False
1540        if utils.is_sequence(c):
1541            usingColScals = True
1542            cc = vtki.vtkUnsignedCharArray()
1543            cc.SetName("TubeColors")
1544            cc.SetNumberOfComponents(3)
1545            cc.SetNumberOfTuples(len(c))
1546            for i, ic in enumerate(c):
1547                r, g, b = get_color(ic)
1548                cc.InsertTuple3(i, int(255 * r), int(255 * g), int(255 * b))
1549            polyln.GetPointData().AddArray(cc)
1550            c = None
1551        tuf.Update()
1552
1553        super().__init__(tuf.GetOutput(), c, alpha)
1554        self.phong()
1555        if usingColScals:
1556            self.mapper.SetScalarModeToUsePointFieldData()
1557            self.mapper.ScalarVisibilityOn()
1558            self.mapper.SelectColorArray("TubeColors")
1559            self.mapper.Modified()
1560        self.name = "Tube"
1561
1562
1563def ThickTube(pts, r1, r2, res=12, c=None, alpha=1.0) -> Union["Mesh", None]:
1564    """
1565    Create a tube with a thickness along a line of points.
1566
1567    Example:
1568    ```python
1569    from vedo import *
1570    pts = [[sin(x), cos(x), x/3] for x in np.arange(0.1, 3, 0.3)]
1571    vline = Line(pts, lw=5, c='red5')
1572    thick_tube = ThickTube(vline, r1=0.2, r2=0.3).lw(1)
1573    show(vline, thick_tube, axes=1).close()
1574    ```
1575    ![](https://vedo.embl.es/images/feats/thick_tube.png)
1576    """
1577
1578    def make_cap(t1, t2):
1579        newpoints = t1.vertices.tolist() + t2.vertices.tolist()
1580        newfaces = []
1581        for i in range(n - 1):
1582            newfaces.append([i, i + 1, i + n])
1583            newfaces.append([i + n, i + 1, i + n + 1])
1584        newfaces.append([2 * n - 1, 0, n])
1585        newfaces.append([2 * n - 1, n - 1, 0])
1586        capm = utils.buildPolyData(newpoints, newfaces)
1587        return capm
1588
1589    assert r1 < r2
1590
1591    t1 = Tube(pts, r=r1, cap=False, res=res)
1592    t2 = Tube(pts, r=r2, cap=False, res=res)
1593
1594    tc1a, tc1b = t1.boundaries().split()
1595    tc2a, tc2b = t2.boundaries().split()
1596    n = tc1b.npoints
1597
1598    tc1b.join(reset=True).clean()  # needed because indices are flipped
1599    tc2b.join(reset=True).clean()
1600
1601    capa = make_cap(tc1a, tc2a)
1602    capb = make_cap(tc1b, tc2b)
1603
1604    thick_tube = merge(t1, t2, capa, capb)
1605    if thick_tube:
1606        thick_tube.c(c).alpha(alpha)
1607        thick_tube.base = t1.base
1608        thick_tube.top  = t1.top
1609        thick_tube.name = "ThickTube"
1610        return thick_tube
1611    return None
1612
1613
1614class Tubes(Mesh):
1615    """
1616    Build tubes around a `Lines` object.
1617    """
1618    def __init__(
1619            self,
1620            lines,
1621            r=1,
1622            vary_radius_by_scalar=False,
1623            vary_radius_by_vector=False,
1624            vary_radius_by_vector_norm=False,
1625            vary_radius_by_absolute_scalar=False,
1626            max_radius_factor=100,
1627            cap=True,
1628            res=12
1629        ) -> None:
1630        """
1631        Wrap tubes around the input `Lines` object.
1632
1633        Arguments:
1634            lines : (Lines)
1635                input Lines object.
1636            r : (float)
1637                constant radius
1638            vary_radius_by_scalar : (bool)
1639                use scalar array to control radius
1640            vary_radius_by_vector : (bool)
1641                use vector array to control radius
1642            vary_radius_by_vector_norm : (bool)
1643                use vector norm to control radius
1644            vary_radius_by_absolute_scalar : (bool)
1645                use absolute scalar value to control radius
1646            max_radius_factor : (float)
1647                max tube radius as a multiple of the min radius
1648            cap : (bool)
1649                capping of the tube
1650            res : (int)
1651                resolution, number of the sides of the tube
1652            c : (color)
1653                constant color or list of colors for each point.
1654        
1655        Examples:
1656            - [streamlines1.py](https://github.com/marcomusy/vedo/blob/master/examples/volumetric/streamlines1.py)
1657        """
1658        plines = lines.dataset
1659        if plines.GetNumberOfLines() == 0:
1660            vedo.logger.warning("Tubes(): input Lines is empty.")
1661
1662        tuf = vtki.new("TubeFilter")
1663        if vary_radius_by_scalar:
1664            tuf.SetVaryRadiusToVaryRadiusByScalar()
1665        elif vary_radius_by_vector:
1666            tuf.SetVaryRadiusToVaryRadiusByVector()
1667        elif vary_radius_by_vector_norm:
1668            tuf.SetVaryRadiusToVaryRadiusByVectorNorm()
1669        elif vary_radius_by_absolute_scalar:
1670            tuf.SetVaryRadiusToVaryRadiusByAbsoluteScalar()
1671        tuf.SetRadius(r)
1672        tuf.SetCapping(cap)
1673        tuf.SetGenerateTCoords(0)
1674        tuf.SetSidesShareVertices(1)
1675        tuf.SetRadiusFactor(max_radius_factor)
1676        tuf.SetNumberOfSides(res)
1677        tuf.SetInputData(plines)
1678        tuf.Update()
1679
1680        super().__init__(tuf.GetOutput())
1681        self.name = "Tubes"
1682    
1683
1684class Ribbon(Mesh):
1685    """
1686    Connect two lines to generate the surface inbetween.
1687    Set the mode by which to create the ruled surface.
1688
1689    It also works with a single line in input. In this case the ribbon
1690    is formed by following the local plane of the line in space.
1691    """
1692
1693    def __init__(
1694        self,
1695        line1,
1696        line2=None,
1697        mode=0,
1698        closed=False,
1699        width=None,
1700        res=(200, 5),
1701        c="indigo3",
1702        alpha=1.0,
1703    ) -> None:
1704        """
1705        Arguments:
1706            mode : (int)
1707                If mode=0, resample evenly the input lines (based on length)
1708                and generates triangle strips.
1709
1710                If mode=1, use the existing points and walks around the
1711                polyline using existing points.
1712
1713            closed : (bool)
1714                if True, join the last point with the first to form a closed surface
1715
1716            res : (list)
1717                ribbon resolutions along the line and perpendicularly to it.
1718
1719        Examples:
1720            - [ribbon.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/ribbon.py)
1721
1722                ![](https://vedo.embl.es/images/basic/ribbon.png)
1723        """
1724
1725        if isinstance(line1, Points):
1726            line1 = line1.vertices
1727
1728        if isinstance(line2, Points):
1729            line2 = line2.vertices
1730
1731        elif line2 is None:
1732            #############################################
1733            ribbon_filter = vtki.new("RibbonFilter")
1734            aline = Line(line1)
1735            ribbon_filter.SetInputData(aline.dataset)
1736            if width is None:
1737                width = aline.diagonal_size() / 20.0
1738            ribbon_filter.SetWidth(width)
1739            ribbon_filter.Update()
1740            # convert triangle strips to polygons
1741            tris = vtki.new("TriangleFilter")
1742            tris.SetInputData(ribbon_filter.GetOutput())
1743            tris.Update()
1744
1745            super().__init__(tris.GetOutput(), c, alpha)
1746            self.name = "Ribbon"
1747            ##############################################
1748            return  ######################################
1749            ##############################################
1750
1751        line1 = np.asarray(line1)
1752        line2 = np.asarray(line2)
1753
1754        if closed:
1755            line1 = line1.tolist()
1756            line1 += [line1[0]]
1757            line2 = line2.tolist()
1758            line2 += [line2[0]]
1759            line1 = np.array(line1)
1760            line2 = np.array(line2)
1761
1762        if len(line1[0]) == 2:
1763            line1 = np.c_[line1, np.zeros(len(line1))]
1764        if len(line2[0]) == 2:
1765            line2 = np.c_[line2, np.zeros(len(line2))]
1766
1767        ppoints1 = vtki.vtkPoints()  # Generate the polyline1
1768        ppoints1.SetData(utils.numpy2vtk(line1, dtype=np.float32))
1769        lines1 = vtki.vtkCellArray()
1770        lines1.InsertNextCell(len(line1))
1771        for i in range(len(line1)):
1772            lines1.InsertCellPoint(i)
1773        poly1 = vtki.vtkPolyData()
1774        poly1.SetPoints(ppoints1)
1775        poly1.SetLines(lines1)
1776
1777        ppoints2 = vtki.vtkPoints()  # Generate the polyline2
1778        ppoints2.SetData(utils.numpy2vtk(line2, dtype=np.float32))
1779        lines2 = vtki.vtkCellArray()
1780        lines2.InsertNextCell(len(line2))
1781        for i in range(len(line2)):
1782            lines2.InsertCellPoint(i)
1783        poly2 = vtki.vtkPolyData()
1784        poly2.SetPoints(ppoints2)
1785        poly2.SetLines(lines2)
1786
1787        # build the lines
1788        lines1 = vtki.vtkCellArray()
1789        lines1.InsertNextCell(poly1.GetNumberOfPoints())
1790        for i in range(poly1.GetNumberOfPoints()):
1791            lines1.InsertCellPoint(i)
1792
1793        polygon1 = vtki.vtkPolyData()
1794        polygon1.SetPoints(ppoints1)
1795        polygon1.SetLines(lines1)
1796
1797        lines2 = vtki.vtkCellArray()
1798        lines2.InsertNextCell(poly2.GetNumberOfPoints())
1799        for i in range(poly2.GetNumberOfPoints()):
1800            lines2.InsertCellPoint(i)
1801
1802        polygon2 = vtki.vtkPolyData()
1803        polygon2.SetPoints(ppoints2)
1804        polygon2.SetLines(lines2)
1805
1806        merged_pd = vtki.new("AppendPolyData")
1807        merged_pd.AddInputData(polygon1)
1808        merged_pd.AddInputData(polygon2)
1809        merged_pd.Update()
1810
1811        rsf = vtki.new("RuledSurfaceFilter")
1812        rsf.CloseSurfaceOff()
1813        rsf.SetRuledMode(mode)
1814        rsf.SetResolution(res[0], res[1])
1815        rsf.SetInputData(merged_pd.GetOutput())
1816        rsf.Update()
1817        # convert triangle strips to polygons
1818        tris = vtki.new("TriangleFilter")
1819        tris.SetInputData(rsf.GetOutput())
1820        tris.Update()
1821        out = tris.GetOutput()
1822
1823        super().__init__(out, c, alpha)
1824
1825        self.name = "Ribbon"
1826
1827
1828class Arrow(Mesh):
1829    """
1830    Build a 3D arrow from `start_pt` to `end_pt` of section size `s`,
1831    expressed as the fraction of the window size.
1832    """
1833
1834    def __init__(
1835        self,
1836        start_pt=(0, 0, 0),
1837        end_pt=(1, 0, 0),
1838        s=None,
1839        shaft_radius=None,
1840        head_radius=None,
1841        head_length=None,
1842        res=12,
1843        c="r4",
1844        alpha=1.0,
1845    ) -> None:
1846        """
1847        If `c` is a `float` less than 1, the arrow is rendered as a in a color scale
1848        from white to red.
1849
1850        .. note:: If `s=None` the arrow is scaled proportionally to its length
1851
1852        ![](https://raw.githubusercontent.com/lorensen/VTKExamples/master/src/Testing/Baseline/Cxx/GeometricObjects/TestOrientedArrow.png)
1853        """
1854        # in case user is passing meshs
1855        if isinstance(start_pt, vtki.vtkActor):
1856            start_pt = start_pt.GetPosition()
1857        if isinstance(end_pt, vtki.vtkActor):
1858            end_pt = end_pt.GetPosition()
1859
1860        axis = np.asarray(end_pt) - np.asarray(start_pt)
1861        length = float(np.linalg.norm(axis))
1862        if length:
1863            axis = axis / length
1864        if len(axis) < 3:  # its 2d
1865            theta = np.pi / 2
1866            start_pt = [start_pt[0], start_pt[1], 0.0]
1867            end_pt = [end_pt[0], end_pt[1], 0.0]
1868        else:
1869            theta = np.arccos(axis[2])
1870        phi = np.arctan2(axis[1], axis[0])
1871        self.source = vtki.new("ArrowSource")
1872        self.source.SetShaftResolution(res)
1873        self.source.SetTipResolution(res)
1874
1875        if s:
1876            sz = 0.02
1877            self.source.SetTipRadius(sz)
1878            self.source.SetShaftRadius(sz / 1.75)
1879            self.source.SetTipLength(sz * 15)
1880
1881        if head_length:
1882            self.source.SetTipLength(head_length)
1883        if head_radius:
1884            self.source.SetTipRadius(head_radius)
1885        if shaft_radius:
1886            self.source.SetShaftRadius(shaft_radius)
1887
1888        self.source.Update()
1889
1890        t = vtki.vtkTransform()
1891        t.Translate(start_pt)
1892        t.RotateZ(np.rad2deg(phi))
1893        t.RotateY(np.rad2deg(theta))
1894        t.RotateY(-90)  # put it along Z
1895        if s:
1896            sz = 800 * s
1897            t.Scale(length, sz, sz)
1898        else:
1899            t.Scale(length, length, length)
1900
1901        tf = vtki.new("TransformPolyDataFilter")
1902        tf.SetInputData(self.source.GetOutput())
1903        tf.SetTransform(t)
1904        tf.Update()
1905
1906        super().__init__(tf.GetOutput(), c, alpha)
1907
1908        self.transform = LinearTransform().translate(start_pt)
1909
1910        self.phong().lighting("plastic")
1911        self.actor.PickableOff()
1912        self.actor.DragableOff()
1913        self.base = np.array(start_pt, dtype=float)  # used by pyplot
1914        self.top  = np.array(end_pt,   dtype=float)  # used by pyplot
1915        self.top_index = self.source.GetTipResolution() * 4
1916        self.fill = True                    # used by pyplot.__iadd__()
1917        self.s = s if s is not None else 1  # used by pyplot.__iadd__()
1918        self.name = "Arrow"
1919    
1920    def top_point(self):
1921        """Return the current coordinates of the tip of the Arrow."""
1922        return self.transform.transform_point(self.top)
1923
1924    def base_point(self):
1925        """Return the current coordinates of the base of the Arrow."""
1926        return self.transform.transform_point(self.base)
1927
1928class Arrows(Glyph):
1929    """
1930    Build arrows between two lists of points.
1931    """
1932
1933    def __init__(
1934        self,
1935        start_pts,
1936        end_pts=None,
1937        s=None,
1938        shaft_radius=None,
1939        head_radius=None,
1940        head_length=None,
1941        thickness=1.0,
1942        res=6,
1943        c='k3',
1944        alpha=1.0,
1945    ) -> None:
1946        """
1947        Build arrows between two lists of points `start_pts` and `end_pts`.
1948         `start_pts` can be also passed in the form `[[point1, point2], ...]`.
1949
1950        Color can be specified as a colormap which maps the size of the arrows.
1951
1952        Arguments:
1953            s : (float)
1954                fix aspect-ratio of the arrow and scale its cross section
1955            c : (color)
1956                color or color map name
1957            alpha : (float)
1958                set object opacity
1959            res : (int)
1960                set arrow resolution
1961
1962        Examples:
1963            - [glyphs2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/glyphs2.py)
1964
1965            ![](https://user-images.githubusercontent.com/32848391/55897850-a1a0da80-5bc1-11e9-81e0-004c8f396b43.jpg)
1966        """
1967        if isinstance(start_pts, Points):
1968            start_pts = start_pts.vertices
1969        if isinstance(end_pts, Points):
1970            end_pts = end_pts.vertices
1971
1972        start_pts = np.asarray(start_pts)
1973        if end_pts is None:
1974            strt = start_pts[:, 0]
1975            end_pts = start_pts[:, 1]
1976            start_pts = strt
1977        else:
1978            end_pts = np.asarray(end_pts)
1979
1980        start_pts = utils.make3d(start_pts)
1981        end_pts = utils.make3d(end_pts)
1982
1983        arr = vtki.new("ArrowSource")
1984        arr.SetShaftResolution(res)
1985        arr.SetTipResolution(res)
1986
1987        if s:
1988            sz = 0.02 * s
1989            arr.SetTipRadius(sz * 2)
1990            arr.SetShaftRadius(sz * thickness)
1991            arr.SetTipLength(sz * 10)
1992
1993        if head_radius:
1994            arr.SetTipRadius(head_radius)
1995        if shaft_radius:
1996            arr.SetShaftRadius(shaft_radius)
1997        if head_length:
1998            arr.SetTipLength(head_length)
1999
2000        arr.Update()
2001        out = arr.GetOutput()
2002
2003        orients = end_pts - start_pts
2004
2005        color_by_vector_size = utils.is_sequence(c) or c in cmaps_names
2006
2007        super().__init__(
2008            start_pts,
2009            out,
2010            orientation_array=orients,
2011            scale_by_vector_size=True,
2012            color_by_vector_size=color_by_vector_size,
2013            c=c,
2014            alpha=alpha,
2015        )
2016        self.lighting("off")
2017        if color_by_vector_size:
2018            vals = np.linalg.norm(orients, axis=1)
2019            self.mapper.SetScalarRange(vals.min(), vals.max())
2020        else:
2021            self.c(c)
2022        self.name = "Arrows"
2023
2024
2025class Arrow2D(Mesh):
2026    """
2027    Build a 2D arrow.
2028    """
2029
2030    def __init__(
2031        self,
2032        start_pt=(0, 0, 0),
2033        end_pt=(1, 0, 0),
2034        s=1,
2035        rotation=0.0,
2036        shaft_length=0.85,
2037        shaft_width=0.055,
2038        head_length=0.175,
2039        head_width=0.175,
2040        fill=True,
2041        c="red4",
2042        alpha=1.0,
2043   ) -> None:
2044        """
2045        Build a 2D arrow from `start_pt` to `end_pt`.
2046
2047        Arguments:
2048            s : (float)
2049                a global multiplicative convenience factor controlling the arrow size
2050            shaft_length : (float)
2051                fractional shaft length
2052            shaft_width : (float)
2053                fractional shaft width
2054            head_length : (float)
2055                fractional head length
2056            head_width : (float)
2057                fractional head width
2058            fill : (bool)
2059                if False only generate the outline
2060        """
2061        self.fill = fill  ## needed by pyplot.__iadd()
2062        self.s = s        ## needed by pyplot.__iadd()
2063
2064        if s != 1:
2065            shaft_width *= s
2066            head_width *= np.sqrt(s)
2067
2068        # in case user is passing meshs
2069        if isinstance(start_pt, vtki.vtkActor):
2070            start_pt = start_pt.GetPosition()
2071        if isinstance(end_pt, vtki.vtkActor):
2072            end_pt = end_pt.GetPosition()
2073        if len(start_pt) == 2:
2074            start_pt = [start_pt[0], start_pt[1], 0]
2075        if len(end_pt) == 2:
2076            end_pt = [end_pt[0], end_pt[1], 0]
2077
2078        headBase = 1 - head_length
2079        head_width = max(head_width, shaft_width)
2080        if head_length is None or headBase > shaft_length:
2081            headBase = shaft_length
2082
2083        verts = []
2084        verts.append([0, -shaft_width / 2, 0])
2085        verts.append([shaft_length, -shaft_width / 2, 0])
2086        verts.append([headBase, -head_width / 2, 0])
2087        verts.append([1, 0, 0])
2088        verts.append([headBase, head_width / 2, 0])
2089        verts.append([shaft_length, shaft_width / 2, 0])
2090        verts.append([0, shaft_width / 2, 0])
2091        if fill:
2092            faces = ((0, 1, 3, 5, 6), (5, 3, 4), (1, 2, 3))
2093            poly = utils.buildPolyData(verts, faces)
2094        else:
2095            lines = (0, 1, 2, 3, 4, 5, 6, 0)
2096            poly = utils.buildPolyData(verts, [], lines=lines)
2097
2098        axis = np.array(end_pt) - np.array(start_pt)
2099        length = float(np.linalg.norm(axis))
2100        if length:
2101            axis = axis / length
2102        theta = 0
2103        if len(axis) > 2:
2104            theta = np.arccos(axis[2])
2105        phi = np.arctan2(axis[1], axis[0])
2106
2107        t = vtki.vtkTransform()
2108        t.Translate(start_pt)
2109        if phi:
2110            t.RotateZ(np.rad2deg(phi))
2111        if theta:
2112            t.RotateY(np.rad2deg(theta))
2113        t.RotateY(-90)  # put it along Z
2114        if rotation:
2115            t.RotateX(rotation)
2116        t.Scale(length, length, length)
2117
2118        tf = vtki.new("TransformPolyDataFilter")
2119        tf.SetInputData(poly)
2120        tf.SetTransform(t)
2121        tf.Update()
2122
2123        super().__init__(tf.GetOutput(), c, alpha)
2124
2125        self.transform = LinearTransform().translate(start_pt)
2126
2127        self.lighting("off")
2128        self.actor.DragableOff()
2129        self.actor.PickableOff()
2130        self.base = np.array(start_pt, dtype=float) # used by pyplot
2131        self.top  = np.array(end_pt,   dtype=float) # used by pyplot
2132        self.name = "Arrow2D"
2133
2134
2135class Arrows2D(Glyph):
2136    """
2137    Build 2D arrows between two lists of points.
2138    """
2139
2140    def __init__(
2141        self,
2142        start_pts,
2143        end_pts=None,
2144        s=1.0,
2145        rotation=0.0,
2146        shaft_length=0.8,
2147        shaft_width=0.05,
2148        head_length=0.225,
2149        head_width=0.175,
2150        fill=True,
2151        c=None,
2152        alpha=1.0,
2153    ) -> None:
2154        """
2155        Build 2D arrows between two lists of points `start_pts` and `end_pts`.
2156        `start_pts` can be also passed in the form `[[point1, point2], ...]`.
2157
2158        Color can be specified as a colormap which maps the size of the arrows.
2159
2160        Arguments:
2161            shaft_length : (float)
2162                fractional shaft length
2163            shaft_width : (float)
2164                fractional shaft width
2165            head_length : (float)
2166                fractional head length
2167            head_width : (float)
2168                fractional head width
2169            fill : (bool)
2170                if False only generate the outline
2171        """
2172        if isinstance(start_pts, Points):
2173            start_pts = start_pts.vertices
2174        if isinstance(end_pts, Points):
2175            end_pts = end_pts.vertices
2176
2177        start_pts = np.asarray(start_pts, dtype=float)
2178        if end_pts is None:
2179            strt = start_pts[:, 0]
2180            end_pts = start_pts[:, 1]
2181            start_pts = strt
2182        else:
2183            end_pts = np.asarray(end_pts, dtype=float)
2184
2185        if head_length is None:
2186            head_length = 1 - shaft_length
2187
2188        arr = Arrow2D(
2189            (0, 0, 0),
2190            (1, 0, 0),
2191            s=s,
2192            rotation=rotation,
2193            shaft_length=shaft_length,
2194            shaft_width=shaft_width,
2195            head_length=head_length,
2196            head_width=head_width,
2197            fill=fill,
2198        )
2199
2200        orients = end_pts - start_pts
2201        orients = utils.make3d(orients)
2202
2203        pts = Points(start_pts)
2204        super().__init__(
2205            pts,
2206            arr,
2207            orientation_array=orients,
2208            scale_by_vector_size=True,
2209            c=c,
2210            alpha=alpha,
2211        )
2212        self.flat().lighting("off").pickable(False)
2213        if c is not None:
2214            self.color(c)
2215        self.name = "Arrows2D"
2216
2217
2218class FlatArrow(Ribbon):
2219    """
2220    Build a 2D arrow in 3D space by joining two close lines.
2221    """
2222
2223    def __init__(self, line1, line2, tip_size=1.0, tip_width=1.0) -> None:
2224        """
2225        Build a 2D arrow in 3D space by joining two close lines.
2226
2227        Examples:
2228            - [flatarrow.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/flatarrow.py)
2229
2230                ![](https://vedo.embl.es/images/basic/flatarrow.png)
2231        """
2232        if isinstance(line1, Points):
2233            line1 = line1.vertices
2234        if isinstance(line2, Points):
2235            line2 = line2.vertices
2236
2237        sm1, sm2 = np.array(line1[-1], dtype=float), np.array(line2[-1], dtype=float)
2238
2239        v = (sm1 - sm2) / 3 * tip_width
2240        p1 = sm1 + v
2241        p2 = sm2 - v
2242        pm1 = (sm1 + sm2) / 2
2243        pm2 = (np.array(line1[-2]) + np.array(line2[-2])) / 2
2244        pm12 = pm1 - pm2
2245        tip = pm12 / np.linalg.norm(pm12) * np.linalg.norm(v) * 3 * tip_size / tip_width + pm1
2246
2247        line1.append(p1)
2248        line1.append(tip)
2249        line2.append(p2)
2250        line2.append(tip)
2251        resm = max(100, len(line1))
2252
2253        super().__init__(line1, line2, res=(resm, 1))
2254        self.phong().lighting("off")
2255        self.actor.PickableOff()
2256        self.actor.DragableOff()
2257        self.name = "FlatArrow"
2258
2259
2260class Triangle(Mesh):
2261    """Create a triangle from 3 points in space."""
2262
2263    def __init__(self, p1, p2, p3, c="green7", alpha=1.0) -> None:
2264        """Create a triangle from 3 points in space."""
2265        super().__init__([[p1, p2, p3], [[0, 1, 2]]], c, alpha)
2266        self.properties.LightingOff()
2267        self.name = "Triangle"
2268
2269
2270class Polygon(Mesh):
2271    """
2272    Build a polygon in the `xy` plane.
2273    """
2274
2275    def __init__(self, pos=(0, 0, 0), nsides=6, r=1.0, c="coral", alpha=1.0) -> None:
2276        """
2277        Build a polygon in the `xy` plane of `nsides` of radius `r`.
2278
2279        ![](https://raw.githubusercontent.com/lorensen/VTKExamples/master/src/Testing/Baseline/Cxx/GeometricObjects/TestRegularPolygonSource.png)
2280        """
2281        t = np.linspace(np.pi / 2, 5 / 2 * np.pi, num=nsides, endpoint=False)
2282        pts = pol2cart(np.ones_like(t) * r, t).T
2283        faces = [list(range(nsides))]
2284        # do not use: vtkRegularPolygonSource
2285        super().__init__([pts, faces], c, alpha)
2286        if len(pos) == 2:
2287            pos = (pos[0], pos[1], 0)
2288        self.pos(pos)
2289        self.properties.LightingOff()
2290        self.name = "Polygon " + str(nsides)
2291
2292
2293class Circle(Polygon):
2294    """
2295    Build a Circle of radius `r`.
2296    """
2297
2298    def __init__(self, pos=(0, 0, 0), r=1.0, res=120, c="gray5", alpha=1.0) -> None:
2299        """
2300        Build a Circle of radius `r`.
2301        """
2302        super().__init__(pos, nsides=res, r=r)
2303
2304        self.nr_of_points = 0
2305        self.va = 0
2306        self.vb = 0
2307        self.axis1: List[float] = []
2308        self.axis2: List[float] = []
2309        self.center: List[float] = []  # filled by pointcloud.pca_ellipse()
2310        self.pvalue = 0.0              # filled by pointcloud.pca_ellipse()
2311        self.alpha(alpha).c(c)
2312        self.name = "Circle"
2313    
2314    def acircularity(self) -> float:
2315        """
2316        Return a measure of how different an ellipse is from a circle.
2317        Values close to zero correspond to a circular object.
2318        """
2319        a, b = self.va, self.vb
2320        value = 0.0
2321        if a+b:
2322            value = ((a-b)/(a+b))**2
2323        return value
2324
2325class GeoCircle(Polygon):
2326    """
2327    Build a Circle of radius `r`.
2328    """
2329
2330    def __init__(self, lat, lon, r=1.0, res=60, c="red4", alpha=1.0) -> None:
2331        """
2332        Build a Circle of radius `r` as projected on a geographic map.
2333        Circles near the poles will look very squashed.
2334
2335        See example:
2336            ```bash
2337            vedo -r earthquake
2338            ```
2339        """
2340        coords = []
2341        sinr, cosr = np.sin(r), np.cos(r)
2342        sinlat, coslat = np.sin(lat), np.cos(lat)
2343        for phi in np.linspace(0, 2 * np.pi, num=res, endpoint=False):
2344            clat = np.arcsin(sinlat * cosr + coslat * sinr * np.cos(phi))
2345            clng = lon + np.arctan2(np.sin(phi) * sinr * coslat, cosr - sinlat * np.sin(clat))
2346            coords.append([clng / np.pi + 1, clat * 2 / np.pi + 1, 0])
2347
2348        super().__init__(nsides=res, c=c, alpha=alpha)
2349        self.vertices = coords # warp polygon points to match geo projection
2350        self.name = "Circle"
2351
2352
2353class Star(Mesh):
2354    """
2355    Build a 2D star shape.
2356    """
2357
2358    def __init__(self, pos=(0, 0, 0), n=5, r1=0.7, r2=1.0, line=False, c="blue6", alpha=1.0) -> None:
2359        """
2360        Build a 2D star shape of `n` cusps of inner radius `r1` and outer radius `r2`.
2361
2362        If line is True then only build the outer line (no internal surface meshing).
2363
2364        Example:
2365            - [extrude.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/extrude.py)
2366
2367                ![](https://vedo.embl.es/images/basic/extrude.png)
2368        """
2369        t = np.linspace(np.pi / 2, 5 / 2 * np.pi, num=n, endpoint=False)
2370        x, y = pol2cart(np.ones_like(t) * r2, t)
2371        pts = np.c_[x, y, np.zeros_like(x)]
2372
2373        apts = []
2374        for i, p in enumerate(pts):
2375            apts.append(p)
2376            if i + 1 < n:
2377                apts.append((p + pts[i + 1]) / 2 * r1 / r2)
2378        apts.append((pts[-1] + pts[0]) / 2 * r1 / r2)
2379
2380        if line:
2381            apts.append(pts[0])
2382            poly = utils.buildPolyData(apts, lines=list(range(len(apts))))
2383            super().__init__(poly, c, alpha)
2384            self.lw(2)
2385        else:
2386            apts.append((0, 0, 0))
2387            cells = []
2388            for i in range(2 * n - 1):
2389                cell = [2 * n, i, i + 1]
2390                cells.append(cell)
2391            cells.append([2 * n, i + 1, 0])
2392            super().__init__([apts, cells], c, alpha)
2393
2394        if len(pos) == 2:
2395            pos = (pos[0], pos[1], 0)
2396
2397        self.properties.LightingOff()
2398        self.name = "Star"
2399
2400
2401class Disc(Mesh):
2402    """
2403    Build a 2D disc.
2404    """
2405
2406    def __init__(
2407        self, pos=(0, 0, 0), r1=0.5, r2=1.0, res=(1, 120), angle_range=(), c="gray4", alpha=1.0
2408    ) -> None:
2409        """
2410        Build a 2D disc of inner radius `r1` and outer radius `r2`.
2411
2412        Set `res` as the resolution in R and Phi (can be a list).
2413
2414        Use `angle_range` to create a disc sector between the 2 specified angles.
2415
2416        ![](https://raw.githubusercontent.com/lorensen/VTKExamples/master/src/Testing/Baseline/Cxx/GeometricObjects/TestDisk.png)
2417        """
2418        if utils.is_sequence(res):
2419            res_r, res_phi = res
2420        else:
2421            res_r, res_phi = res, 12 * res
2422
2423        if len(angle_range) == 0:
2424            ps = vtki.new("DiskSource")
2425        else:
2426            ps = vtki.new("SectorSource")
2427            ps.SetStartAngle(angle_range[0])
2428            ps.SetEndAngle(angle_range[1])
2429
2430        ps.SetInnerRadius(r1)
2431        ps.SetOuterRadius(r2)
2432        ps.SetRadialResolution(res_r)
2433        ps.SetCircumferentialResolution(res_phi)
2434        ps.Update()
2435        super().__init__(ps.GetOutput(), c, alpha)
2436        self.flat()
2437        self.pos(utils.make3d(pos))
2438        self.name = "Disc"
2439
2440
2441class Arc(Mesh):
2442    """
2443    Build a 2D circular arc between 2 points.
2444    """
2445
2446    def __init__(
2447        self,
2448        center,
2449        point1,
2450        point2=None,
2451        normal=None,
2452        angle=None,
2453        invert=False,
2454        res=50,
2455        c="gray4",
2456        alpha=1.0,
2457    ) -> None:
2458        """
2459        Build a 2D circular arc between 2 points `point1` and `point2`.
2460
2461        If `normal` is specified then `center` is ignored, and
2462        normal vector, a starting `point1` (polar vector)
2463        and an angle defining the arc length need to be assigned.
2464
2465        Arc spans the shortest angular sector point1 and point2,
2466        if `invert=True`, then the opposite happens.
2467        """
2468        if len(point1) == 2:
2469            point1 = (point1[0], point1[1], 0)
2470        if point2 is not None and len(point2) == 2:
2471            point2 = (point2[0], point2[1], 0)
2472
2473        ar = vtki.new("ArcSource")
2474        if point2 is not None:
2475            self.top = point2
2476            point2 = point2 - np.asarray(point1)
2477            ar.UseNormalAndAngleOff()
2478            ar.SetPoint1([0, 0, 0])
2479            ar.SetPoint2(point2)
2480            # ar.SetCenter(center)
2481        elif normal is not None and angle is not None:
2482            ar.UseNormalAndAngleOn()
2483            ar.SetAngle(angle)
2484            ar.SetPolarVector(point1)
2485            ar.SetNormal(normal)
2486        else:
2487            vedo.logger.error("incorrect input combination")
2488            return
2489        ar.SetNegative(invert)
2490        ar.SetResolution(res)
2491        ar.Update()
2492
2493        super().__init__(ar.GetOutput(), c, alpha)
2494        self.pos(center)
2495        self.lw(2).lighting("off")
2496        self.name = "Arc"
2497
2498
2499class IcoSphere(Mesh):
2500    """
2501    Create a sphere made of a uniform triangle mesh.
2502    """
2503
2504    def __init__(self, pos=(0, 0, 0), r=1.0, subdivisions=4, c="r5", alpha=1.0) -> None:
2505        """
2506        Create a sphere made of a uniform triangle mesh
2507        (from recursive subdivision of an icosahedron).
2508
2509        Example:
2510        ```python
2511        from vedo import *
2512        icos = IcoSphere(subdivisions=3)
2513        icos.compute_quality().cmap('coolwarm')
2514        icos.show(axes=1).close()
2515        ```
2516        ![](https://vedo.embl.es/images/basic/icosphere.jpg)
2517        """
2518        subdivisions = int(min(subdivisions, 9))  # to avoid disasters
2519
2520        t = (1.0 + np.sqrt(5.0)) / 2.0
2521        points = np.array(
2522            [
2523                [-1, t, 0],
2524                [1, t, 0],
2525                [-1, -t, 0],
2526                [1, -t, 0],
2527                [0, -1, t],
2528                [0, 1, t],
2529                [0, -1, -t],
2530                [0, 1, -t],
2531                [t, 0, -1],
2532                [t, 0, 1],
2533                [-t, 0, -1],
2534                [-t, 0, 1],
2535            ]
2536        )
2537        faces = [
2538            [0, 11, 5],
2539            [0, 5, 1],
2540            [0, 1, 7],
2541            [0, 7, 10],
2542            [0, 10, 11],
2543            [1, 5, 9],
2544            [5, 11, 4],
2545            [11, 10, 2],
2546            [10, 7, 6],
2547            [7, 1, 8],
2548            [3, 9, 4],
2549            [3, 4, 2],
2550            [3, 2, 6],
2551            [3, 6, 8],
2552            [3, 8, 9],
2553            [4, 9, 5],
2554            [2, 4, 11],
2555            [6, 2, 10],
2556            [8, 6, 7],
2557            [9, 8, 1],
2558        ]
2559        super().__init__([points * r, faces], c=c, alpha=alpha)
2560
2561        for _ in range(subdivisions):
2562            self.subdivide(method=1)
2563            pts = utils.versor(self.vertices) * r
2564            self.vertices = pts
2565
2566        self.pos(pos)
2567        self.name = "IcoSphere"
2568
2569
2570class Sphere(Mesh):
2571    """
2572    Build a sphere.
2573    """
2574
2575    def __init__(self, pos=(0, 0, 0), r=1.0, res=24, quads=False, c="r5", alpha=1.0) -> None:
2576        """
2577        Build a sphere at position `pos` of radius `r`.
2578
2579        Arguments:
2580            r : (float)
2581                sphere radius
2582            res : (int, list)
2583                resolution in phi, resolution in theta is by default `2*res`
2584            quads : (bool)
2585                sphere mesh will be made of quads instead of triangles
2586
2587        [](https://user-images.githubusercontent.com/32848391/72433092-f0a31e00-3798-11ea-85f7-b2f5fcc31568.png)
2588        """
2589        if len(pos) == 2:
2590            pos = np.asarray([pos[0], pos[1], 0])
2591
2592        self.radius = r  # used by fitSphere
2593        self.center = pos
2594        self.residue = 0
2595
2596        if quads:
2597            res = max(res, 4)
2598            img = vtki.vtkImageData()
2599            img.SetDimensions(res - 1, res - 1, res - 1)
2600            rs = 1.0 / (res - 2)
2601            img.SetSpacing(rs, rs, rs)
2602            gf = vtki.new("GeometryFilter")
2603            gf.SetInputData(img)
2604            gf.Update()
2605            super().__init__(gf.GetOutput(), c, alpha)
2606            self.lw(0.1)
2607
2608            cgpts = self.vertices - (0.5, 0.5, 0.5)
2609
2610            x, y, z = cgpts.T
2611            x = x * (1 + x * x) / 2
2612            y = y * (1 + y * y) / 2
2613            z = z * (1 + z * z) / 2
2614            _, theta, phi = cart2spher(x, y, z)
2615
2616            pts = spher2cart(np.ones_like(phi) * r, theta, phi).T
2617            self.vertices = pts
2618
2619        else:
2620            if utils.is_sequence(res):
2621                res_t, res_phi = res
2622            else:
2623                res_t, res_phi = 2 * res, res
2624
2625            ss = vtki.new("SphereSource")
2626            ss.SetRadius(r)
2627            ss.SetThetaResolution(res_t)
2628            ss.SetPhiResolution(res_phi)
2629            ss.Update()
2630
2631            super().__init__(ss.GetOutput(), c, alpha)
2632
2633        self.phong()
2634        self.pos(pos)
2635        self.name = "Sphere"
2636
2637
2638class Spheres(Mesh):
2639    """
2640    Build a large set of spheres.
2641    """
2642
2643    def __init__(self, centers, r=1.0, res=8, c="red5", alpha=1) -> None:
2644        """
2645        Build a (possibly large) set of spheres at `centers` of radius `r`.
2646
2647        Either `c` or `r` can be a list of RGB colors or radii.
2648
2649        Examples:
2650            - [manyspheres.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/manyspheres.py)
2651
2652            ![](https://vedo.embl.es/images/basic/manyspheres.jpg)
2653        """
2654
2655        if isinstance(centers, Points):
2656            centers = centers.vertices
2657        centers = np.asarray(centers, dtype=float)
2658        base = centers[0]
2659
2660        cisseq = False
2661        if utils.is_sequence(c):
2662            cisseq = True
2663
2664        if cisseq:
2665            if len(centers) != len(c):
2666                vedo.logger.error(f"mismatch #centers {len(centers)} != {len(c)} #colors")
2667                raise RuntimeError()
2668
2669        risseq = False
2670        if utils.is_sequence(r):
2671            risseq = True
2672
2673        if risseq:
2674            if len(centers) != len(r):
2675                vedo.logger.error(f"mismatch #centers {len(centers)} != {len(r)} #radii")
2676                raise RuntimeError()
2677        if cisseq and risseq:
2678            vedo.logger.error("Limitation: c and r cannot be both sequences.")
2679            raise RuntimeError()
2680
2681        src = vtki.new("SphereSource")
2682        if not risseq:
2683            src.SetRadius(r)
2684        if utils.is_sequence(res):
2685            res_t, res_phi = res
2686        else:
2687            res_t, res_phi = 2 * res, res
2688
2689        src.SetThetaResolution(res_t)
2690        src.SetPhiResolution(res_phi)
2691        src.Update()
2692
2693        psrc = vtki.new("PointSource")
2694        psrc.SetNumberOfPoints(len(centers))
2695        psrc.Update()
2696        pd = psrc.GetOutput()
2697        vpts = pd.GetPoints()
2698
2699        glyph = vtki.vtkGlyph3D()
2700        glyph.SetSourceConnection(src.GetOutputPort())
2701
2702        if cisseq:
2703            glyph.SetColorModeToColorByScalar()
2704            ucols = vtki.vtkUnsignedCharArray()
2705            ucols.SetNumberOfComponents(3)
2706            ucols.SetName("Colors")
2707            for acol in c:
2708                cx, cy, cz = get_color(acol)
2709                ucols.InsertNextTuple3(cx * 255, cy * 255, cz * 255)
2710            pd.GetPointData().AddArray(ucols)
2711            pd.GetPointData().SetActiveScalars("Colors")
2712            glyph.ScalingOff()
2713        elif risseq:
2714            glyph.SetScaleModeToScaleByScalar()
2715            urads = utils.numpy2vtk(2 * np.ascontiguousarray(r), dtype=np.float32)
2716            urads.SetName("Radii")
2717            pd.GetPointData().AddArray(urads)
2718            pd.GetPointData().SetActiveScalars("Radii")
2719
2720        vpts.SetData(utils.numpy2vtk(centers - base, dtype=np.float32))
2721
2722        glyph.SetInputData(pd)
2723        glyph.Update()
2724
2725        super().__init__(glyph.GetOutput(), alpha=alpha)
2726        self.pos(base)
2727        self.phong()
2728        if cisseq:
2729            self.mapper.ScalarVisibilityOn()
2730        else:
2731            self.mapper.ScalarVisibilityOff()
2732            self.c(c)
2733        self.name = "Spheres"
2734
2735
2736class Earth(Mesh):
2737    """
2738    Build a textured mesh representing the Earth.
2739    """
2740
2741    def __init__(self, style=1, r=1.0) -> None:
2742        """
2743        Build a textured mesh representing the Earth.
2744
2745        Example:
2746            - [geodesic_curve.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/geodesic_curve.py)
2747
2748                ![](https://vedo.embl.es/images/advanced/geodesic.png)
2749        """
2750        tss = vtki.new("TexturedSphereSource")
2751        tss.SetRadius(r)
2752        tss.SetThetaResolution(72)
2753        tss.SetPhiResolution(36)
2754        tss.Update()
2755        super().__init__(tss.GetOutput(), c="w")
2756        atext = vtki.vtkTexture()
2757        pnm_reader = vtki.new("JPEGReader")
2758        fn = vedo.file_io.download(vedo.dataurl + f"textures/earth{style}.jpg", verbose=False)
2759        pnm_reader.SetFileName(fn)
2760        atext.SetInputConnection(pnm_reader.GetOutputPort())
2761        atext.InterpolateOn()
2762        self.texture(atext)
2763        self.name = "Earth"
2764
2765
2766class Ellipsoid(Mesh):
2767    """Build a 3D ellipsoid."""
2768    def __init__(
2769        self,
2770        pos=(0, 0, 0),
2771        axis1=(0.5, 0, 0),
2772        axis2=(0, 1, 0),
2773        axis3=(0, 0, 1.5),
2774        res=24,
2775        c="cyan4",
2776        alpha=1.0,
2777    ) -> None:
2778        """
2779        Build a 3D ellipsoid centered at position `pos`.
2780
2781        Arguments:
2782            axis1 : (list)
2783                First axis. Length corresponds to semi-axis.
2784            axis2 : (list)
2785                Second axis. Length corresponds to semi-axis.
2786            axis3 : (list)
2787                Third axis. Length corresponds to semi-axis.
2788        """        
2789        self.center = utils.make3d(pos)
2790
2791        self.axis1 = utils.make3d(axis1)
2792        self.axis2 = utils.make3d(axis2)
2793        self.axis3 = utils.make3d(axis3)
2794
2795        self.va = np.linalg.norm(self.axis1)
2796        self.vb = np.linalg.norm(self.axis2)
2797        self.vc = np.linalg.norm(self.axis3)
2798
2799        self.va_error = 0
2800        self.vb_error = 0
2801        self.vc_error = 0
2802
2803        self.nr_of_points = 1  # used by pointcloud.pca_ellipsoid()
2804        self.pvalue = 0        # used by pointcloud.pca_ellipsoid()
2805
2806        if utils.is_sequence(res):
2807            res_t, res_phi = res
2808        else:
2809            res_t, res_phi = 2 * res, res
2810
2811        elli_source = vtki.new("SphereSource")
2812        elli_source.SetRadius(1)
2813        elli_source.SetThetaResolution(res_t)
2814        elli_source.SetPhiResolution(res_phi)
2815        elli_source.Update()
2816
2817        super().__init__(elli_source.GetOutput(), c, alpha)
2818
2819        matrix = np.c_[self.axis1, self.axis2, self.axis3]
2820        lt = LinearTransform(matrix).translate(pos)
2821        self.apply_transform(lt)
2822        self.name = "Ellipsoid"
2823
2824    def asphericity(self) -> float:
2825        """
2826        Return a measure of how different an ellipsoid is from a sphere.
2827        Values close to zero correspond to a spheric object.
2828        """
2829        a, b, c = self.va, self.vb, self.vc
2830        asp = ( ((a-b)/(a+b))**2
2831              + ((a-c)/(a+c))**2
2832              + ((b-c)/(b+c))**2 ) / 3. * 4.
2833        return float(asp)
2834
2835    def asphericity_error(self) -> float:
2836        """
2837        Calculate statistical error on the asphericity value.
2838
2839        Errors on the main axes are stored in
2840        `Ellipsoid.va_error`, Ellipsoid.vb_error` and `Ellipsoid.vc_error`.
2841        """
2842        a, b, c = self.va, self.vb, self.vc
2843        sqrtn = np.sqrt(self.nr_of_points)
2844        ea, eb, ec = a / 2 / sqrtn, b / 2 / sqrtn, b / 2 / sqrtn
2845
2846        # from sympy import *
2847        # init_printing(use_unicode=True)
2848        # a, b, c, ea, eb, ec = symbols("a b c, ea, eb,ec")
2849        # L = (
2850        #    (((a - b) / (a + b)) ** 2 + ((c - b) / (c + b)) ** 2 + ((a - c) / (a + c)) ** 2)
2851        #    / 3 * 4)
2852        # dl2 = (diff(L, a) * ea) ** 2 + (diff(L, b) * eb) ** 2 + (diff(L, c) * ec) ** 2
2853        # print(dl2)
2854        # exit()
2855
2856        dL2 = (
2857            ea ** 2
2858            * (
2859                -8 * (a - b) ** 2 / (3 * (a + b) ** 3)
2860                - 8 * (a - c) ** 2 / (3 * (a + c) ** 3)
2861                + 4 * (2 * a - 2 * c) / (3 * (a + c) ** 2)
2862                + 4 * (2 * a - 2 * b) / (3 * (a + b) ** 2)
2863            ) ** 2
2864            + eb ** 2
2865            * (
2866                4 * (-2 * a + 2 * b) / (3 * (a + b) ** 2)
2867                - 8 * (a - b) ** 2 / (3 * (a + b) ** 3)
2868                - 8 * (-b + c) ** 2 / (3 * (b + c) ** 3)
2869                + 4 * (2 * b - 2 * c) / (3 * (b + c) ** 2)
2870            ) ** 2
2871            + ec ** 2
2872            * (
2873                4 * (-2 * a + 2 * c) / (3 * (a + c) ** 2)
2874                - 8 * (a - c) ** 2 / (3 * (a + c) ** 3)
2875                + 4 * (-2 * b + 2 * c) / (3 * (b + c) ** 2)
2876                - 8 * (-b + c) ** 2 / (3 * (b + c) ** 3)
2877            ) ** 2
2878        )
2879        err = np.sqrt(dL2)
2880        self.va_error = ea
2881        self.vb_error = eb
2882        self.vc_error = ec
2883        return err
2884
2885
2886class Grid(Mesh):
2887    """
2888    An even or uneven 2D grid.
2889    """
2890
2891    def __init__(self, pos=(0, 0, 0), s=(1, 1), res=(10, 10), lw=1, c="k3", alpha=1.0) -> None:
2892        """
2893        Create an even or uneven 2D grid.
2894        Can also be created from a `np.mgrid` object (see example).
2895
2896        Arguments:
2897            pos : (list, Points, Mesh)
2898                position in space, can also be passed as a bounding box [xmin,xmax, ymin,ymax].
2899            s : (float, list)
2900                if a float is provided it is interpreted as the total size along x and y,
2901                if a list of coords is provided they are interpreted as the vertices of the grid along x and y.
2902                In this case keyword `res` is ignored (see example below).
2903            res : (list)
2904                resolutions along x and y, e.i. the number of subdivisions
2905            lw : (int)
2906                line width
2907
2908        Example:
2909            ```python
2910            from vedo import *
2911            xcoords = np.arange(0, 2, 0.2)
2912            ycoords = np.arange(0, 1, 0.2)
2913            sqrtx = sqrt(xcoords)
2914            grid = Grid(s=(sqrtx, ycoords)).lw(2)
2915            grid.show(axes=8).close()
2916
2917            # Can also create a grid from a np.mgrid:
2918            X, Y = np.mgrid[-12:12:10*1j, 200:215:10*1j]
2919            vgrid = Grid(s=(X[:,0], Y[0]))
2920            vgrid.show(axes=8).close()
2921            ```
2922            ![](https://vedo.embl.es/images/feats/uneven_grid.png)
2923        """
2924        resx, resy = res
2925        sx, sy = s
2926        
2927        try:
2928            bb = pos.bounds()
2929            pos = [(bb[0] + bb[1])/2, (bb[2] + bb[3])/2, (bb[4] + bb[5])/2]
2930            sx = bb[1] - bb[0]
2931            sy = bb[3] - bb[2]
2932        except AttributeError:
2933            pass        
2934
2935        if len(pos) == 2:
2936            pos = (pos[0], pos[1], 0)
2937        elif len(pos) in [4,6]: # passing a bounding box
2938            bb = pos
2939            pos = [(bb[0] + bb[1])/2, (bb[2] + bb[3])/2, 0]
2940            sx = bb[1] - bb[0]
2941            sy = bb[3] - bb[2]
2942            if len(pos)==6:
2943                pos[2] = bb[4] - bb[5]
2944
2945        if utils.is_sequence(sx) and utils.is_sequence(sy):
2946            verts = []
2947            for y in sy:
2948                for x in sx:
2949                    verts.append([x, y, 0])
2950            faces = []
2951            n = len(sx)
2952            m = len(sy)
2953            for j in range(m - 1):
2954                j1n = (j + 1) * n
2955                for i in range(n - 1):
2956                    faces.append([i + j * n, i + 1 + j * n, i + 1 + j1n, i + j1n])
2957
2958            super().__init__([verts, faces], c, alpha)
2959
2960        else:
2961            ps = vtki.new("PlaneSource")
2962            ps.SetResolution(resx, resy)
2963            ps.Update()
2964
2965            t = vtki.vtkTransform()
2966            t.Translate(pos)
2967            t.Scale(sx, sy, 1)
2968
2969            tf = vtki.new("TransformPolyDataFilter")
2970            tf.SetInputData(ps.GetOutput())
2971            tf.SetTransform(t)
2972            tf.Update()
2973
2974            super().__init__(tf.GetOutput(), c, alpha)
2975
2976        self.wireframe().lw(lw)
2977        self.properties.LightingOff()
2978        self.name = "Grid"
2979
2980
2981class Plane(Mesh):
2982    """Create a plane in space."""
2983
2984    def __init__(
2985            self,
2986            pos=(0, 0, 0),
2987            normal=(0, 0, 1),
2988            s=(1, 1),
2989            res=(1, 1),
2990            c="gray5", alpha=1.0,
2991        ) -> None:
2992        """
2993        Create a plane of size `s=(xsize, ysize)` oriented perpendicular
2994        to vector `normal` so that it passes through point `pos`.
2995
2996        Arguments:
2997            pos : (list)
2998                position of the plane center
2999            normal : (list)
3000                normal vector to the plane
3001            s : (list)
3002                size of the plane along x and y
3003            res : (list)
3004                resolution of the plane along x and y
3005        """
3006        if isinstance(pos, vtki.vtkPolyData):
3007            super().__init__(pos, c, alpha)
3008            # self.transform = LinearTransform().translate(pos)
3009
3010        else:
3011            ps = vtki.new("PlaneSource")
3012            ps.SetResolution(res[0], res[1])
3013            tri = vtki.new("TriangleFilter")
3014            tri.SetInputConnection(ps.GetOutputPort())
3015            tri.Update()
3016            
3017            super().__init__(tri.GetOutput(), c, alpha)
3018
3019            pos = utils.make3d(pos)
3020            normal = np.asarray(normal, dtype=float)
3021            axis = normal / np.linalg.norm(normal)
3022            theta = np.arccos(axis[2])
3023            phi = np.arctan2(axis[1], axis[0])
3024
3025            t = LinearTransform()
3026            t.scale([s[0], s[1], 1])
3027            t.rotate_y(np.rad2deg(theta))
3028            t.rotate_z(np.rad2deg(phi))
3029            t.translate(pos)
3030            self.apply_transform(t)
3031
3032        self.lighting("off")
3033        self.name = "Plane"
3034        self.variance = 0
3035
3036    def clone(self, deep=True) -> "Plane":
3037        newplane = Plane()
3038        if deep:
3039            newplane.dataset.DeepCopy(self.dataset)
3040        else:
3041            newplane.dataset.ShallowCopy(self.dataset)
3042        newplane.copy_properties_from(self)
3043        newplane.transform = self.transform.clone()
3044        newplane.variance = 0
3045        return newplane
3046    
3047    @property
3048    def normal(self) -> np.ndarray:
3049        pts = self.vertices
3050        # this is necessary because plane can have high resolution
3051        # p0, p1 = pts[0], pts[1]
3052        # AB = p1 - p0
3053        # AB /= np.linalg.norm(AB)
3054        # for pt in pts[2:]:
3055        #     AC = pt - p0
3056        #     AC /= np.linalg.norm(AC)
3057        #     cosine_angle = np.dot(AB, AC)
3058        #     if abs(cosine_angle) < 0.99:
3059        #         normal = np.cross(AB, AC)
3060        #         return normal / np.linalg.norm(normal)
3061        p0, p1, p2 = pts[0], pts[1], pts[int(len(pts)/2 +0.5)]
3062        AB = p1 - p0
3063        AB /= np.linalg.norm(AB)
3064        AC = p2 - p0
3065        AC /= np.linalg.norm(AC)
3066        normal = np.cross(AB, AC)
3067        return normal / np.linalg.norm(normal)
3068
3069    @property
3070    def center(self) -> np.ndarray:
3071        pts = self.vertices
3072        return np.mean(pts, axis=0)
3073
3074    def contains(self, points, tol=0) -> np.ndarray:
3075        """
3076        Check if each of the provided point lies on this plane.
3077        `points` is an array of shape (n, 3).
3078        """
3079        points = np.array(points, dtype=float)
3080        bounds = self.vertices
3081
3082        mask = np.isclose(np.dot(points - self.center, self.normal), 0, atol=tol)
3083
3084        for i in [1, 3]:
3085            AB = bounds[i] - bounds[0]
3086            AP = points - bounds[0]
3087            mask_l = np.less_equal(np.dot(AP, AB), np.linalg.norm(AB))
3088            mask_g = np.greater_equal(np.dot(AP, AB), 0)
3089            mask = np.logical_and(mask, mask_l)
3090            mask = np.logical_and(mask, mask_g)
3091        return mask
3092
3093
3094class Rectangle(Mesh):
3095    """
3096    Build a rectangle in the xy plane.
3097    """
3098
3099    def __init__(self, p1=(0, 0), p2=(1, 1), radius=None, res=12, c="gray5", alpha=1.0) -> None:
3100        """
3101        Build a rectangle in the xy plane identified by any two corner points.
3102
3103        Arguments:
3104            p1 : (list)
3105                bottom-left position of the corner
3106            p2 : (list)
3107                top-right position of the corner
3108            radius : (float, list)
3109                smoothing radius of the corner in world units.
3110                A list can be passed with 4 individual values.
3111        """
3112        if len(p1) == 2:
3113            p1 = np.array([p1[0], p1[1], 0.0])
3114        else:
3115            p1 = np.array(p1, dtype=float)
3116        if len(p2) == 2:
3117            p2 = np.array([p2[0], p2[1], 0.0])
3118        else:
3119            p2 = np.array(p2, dtype=float)
3120
3121        self.corner1 = p1
3122        self.corner2 = p2
3123
3124        color = c
3125        smoothr = False
3126        risseq = False
3127        if utils.is_sequence(radius):
3128            risseq = True
3129            smoothr = True
3130            if max(radius) == 0:
3131                smoothr = False
3132        elif radius:
3133            smoothr = True
3134
3135        if not smoothr:
3136            radius = None
3137        self.radius = radius
3138
3139        if smoothr:
3140            r = radius
3141            if not risseq:
3142                r = [r, r, r, r]
3143            rd, ra, rb, rc = r
3144
3145            if p1[0] > p2[0]:  # flip p1 - p2
3146                p1, p2 = p2, p1
3147            if p1[1] > p2[1]:  # flip p1y - p2y
3148                p1[1], p2[1] = p2[1], p1[1]
3149
3150            px, py, _ = p2 - p1
3151            k = min(px / 2, py / 2)
3152            ra = min(abs(ra), k)
3153            rb = min(abs(rb), k)
3154            rc = min(abs(rc), k)
3155            rd = min(abs(rd), k)
3156            beta = np.linspace(0, 2 * np.pi, num=res * 4, endpoint=False)
3157            betas = np.split(beta, 4)
3158            rrx = np.cos(betas)
3159            rry = np.sin(betas)
3160
3161            q1 = (rd, 0)
3162            # q2 = (px-ra, 0)
3163            q3 = (px, ra)
3164            # q4 = (px, py-rb)
3165            q5 = (px - rb, py)
3166            # q6 = (rc, py)
3167            q7 = (0, py - rc)
3168            # q8 = (0, rd)
3169            a = np.c_[rrx[3], rry[3]]*ra + [px-ra, ra]    if ra else np.array([])
3170            b = np.c_[rrx[0], rry[0]]*rb + [px-rb, py-rb] if rb else np.array([])
3171            c = np.c_[rrx[1], rry[1]]*rc + [rc, py-rc]    if rc else np.array([])
3172            d = np.c_[rrx[2], rry[2]]*rd + [rd, rd]       if rd else np.array([])
3173
3174            pts = [q1, *a.tolist(), q3, *b.tolist(), q5, *c.tolist(), q7, *d.tolist()]
3175            faces = [list(range(len(pts)))]
3176        else:
3177            p1r = np.array([p2[0], p1[1], 0.0])
3178            p2l = np.array([p1[0], p2[1], 0.0])
3179            pts = ([0.0, 0.0, 0.0], p1r - p1, p2 - p1, p2l - p1)
3180            faces = [(0, 1, 2, 3)]
3181
3182        super().__init__([pts, faces], color, alpha)
3183        self.pos(p1)
3184        self.properties.LightingOff()
3185        self.name = "Rectangle"
3186
3187
3188class Box(Mesh):
3189    """
3190    Build a box of specified dimensions.
3191    """
3192
3193    def __init__(
3194            self, pos=(0, 0, 0), 
3195            length=1.0, width=2.0, height=3.0, size=(), c="g4", alpha=1.0) -> None:
3196        """
3197        Build a box of dimensions `x=length, y=width and z=height`.
3198        Alternatively dimensions can be defined by setting `size` keyword with a tuple.
3199
3200        If `pos` is a list of 6 numbers, this will be interpreted as the bounding box:
3201        `[xmin,xmax, ymin,ymax, zmin,zmax]`
3202
3203        Note that the shape polygonal data contains duplicated vertices. This is to allow
3204        each face to have its own normal, which is essential for some operations.
3205        Use the `clean()` method to remove duplicate points.
3206
3207        Examples:
3208            - [aspring1.py](https://github.com/marcomusy/vedo/tree/master/examples/simulations/aspring1.py)
3209
3210                ![](https://vedo.embl.es/images/simulations/50738955-7e891800-11d9-11e9-85cd-02bd4f3f13ea.gif)
3211        """
3212        src = vtki.new("CubeSource")
3213
3214        if len(pos) == 2:
3215            pos = (pos[0], pos[1], 0)
3216
3217        if len(pos) == 6:
3218            src.SetBounds(pos)
3219            pos = [(pos[0] + pos[1]) / 2, (pos[2] + pos[3]) / 2, (pos[4] + pos[5]) / 2]
3220        elif len(size) == 3:
3221            length, width, height = size
3222            src.SetXLength(length)
3223            src.SetYLength(width)
3224            src.SetZLength(height)
3225            src.SetCenter(pos)
3226        else:
3227            src.SetXLength(length)
3228            src.SetYLength(width)
3229            src.SetZLength(height)
3230            src.SetCenter(pos)
3231
3232        src.Update()
3233        pd = src.GetOutput()
3234
3235        tc = [
3236            [0.0, 0.0],
3237            [1.0, 0.0],
3238            [0.0, 1.0],
3239            [1.0, 1.0],
3240            [1.0, 0.0],
3241            [0.0, 0.0],
3242            [1.0, 1.0],
3243            [0.0, 1.0],
3244            [1.0, 1.0],
3245            [1.0, 0.0],
3246            [0.0, 1.0],
3247            [0.0, 0.0],
3248            [0.0, 1.0],
3249            [0.0, 0.0],
3250            [1.0, 1.0],
3251            [1.0, 0.0],
3252            [1.0, 0.0],
3253            [0.0, 0.0],
3254            [1.0, 1.0],
3255            [0.0, 1.0],
3256            [0.0, 0.0],
3257            [1.0, 0.0],
3258            [0.0, 1.0],
3259            [1.0, 1.0],
3260        ]
3261        vtc = utils.numpy2vtk(tc)
3262        pd.GetPointData().SetTCoords(vtc)
3263        super().__init__(pd, c, alpha)
3264        self.transform = LinearTransform().translate(pos)
3265        self.name = "Box"
3266
3267
3268class Cube(Box):
3269    """
3270    Build a cube shape.
3271    
3272    Note that the shape polygonal data contains duplicated vertices. This is to allow
3273    each face to have its own normal, which is essential for some operations.
3274    Use the `clean()` method to remove duplicate points.
3275    """
3276
3277    def __init__(self, pos=(0, 0, 0), side=1.0, c="g4", alpha=1.0) -> None:
3278        """Build a cube of size `side`."""
3279        super().__init__(pos, side, side, side, (), c, alpha)
3280        self.name = "Cube"
3281
3282
3283class TessellatedBox(Mesh):
3284    """
3285    Build a cubic `Mesh` made of quads.
3286    """
3287
3288    def __init__(self, pos=(0, 0, 0), n=10, spacing=(1, 1, 1), bounds=(), c="k5", alpha=0.5) -> None:
3289        """
3290        Build a cubic `Mesh` made of `n` small quads in the 3 axis directions.
3291
3292        Arguments:
3293            pos : (list)
3294                position of the left bottom corner
3295            n : (int, list)
3296                number of subdivisions along each side
3297            spacing : (float)
3298                size of the side of the single quad in the 3 directions
3299        """
3300        if utils.is_sequence(n):  # slow
3301            img = vtki.vtkImageData()
3302            img.SetDimensions(n[0] + 1, n[1] + 1, n[2] + 1)
3303            img.SetSpacing(spacing)
3304            gf = vtki.new("GeometryFilter")
3305            gf.SetInputData(img)
3306            gf.Update()
3307            poly = gf.GetOutput()
3308        else:  # fast
3309            n -= 1
3310            tbs = vtki.new("TessellatedBoxSource")
3311            tbs.SetLevel(n)
3312            if len(bounds):
3313                tbs.SetBounds(bounds)
3314            else:
3315                tbs.SetBounds(0, n * spacing[0], 0, n * spacing[1], 0, n * spacing[2])
3316            tbs.QuadsOn()
3317            #tbs.SetOutputPointsPrecision(vtki.vtkAlgorithm.SINGLE_PRECISION)
3318            tbs.Update()
3319            poly = tbs.GetOutput()
3320        super().__init__(poly, c=c, alpha=alpha)
3321        self.pos(pos)
3322        self.lw(1).lighting("off")
3323        self.name = "TessellatedBox"
3324
3325
3326class Spring(Mesh):
3327    """
3328    Build a spring model.
3329    """
3330
3331    def __init__(
3332        self,
3333        start_pt=(0, 0, 0),
3334        end_pt=(1, 0, 0),
3335        coils=20,
3336        r1=0.1,
3337        r2=None,
3338        thickness=None,
3339        c="gray5",
3340        alpha=1.0,
3341    ) -> None:
3342        """
3343        Build a spring of specified nr of `coils` between `start_pt` and `end_pt`.
3344
3345        Arguments:
3346            coils : (int)
3347                number of coils
3348            r1 : (float)
3349                radius at start point
3350            r2 : (float)
3351                radius at end point
3352            thickness : (float)
3353                thickness of the coil section
3354        """
3355        start_pt = utils.make3d(start_pt)
3356        end_pt = utils.make3d(end_pt)
3357
3358        diff = end_pt - start_pt
3359        length = np.linalg.norm(diff)
3360        if not length:
3361            return
3362        if not r1:
3363            r1 = length / 20
3364        trange = np.linspace(0, length, num=50 * coils)
3365        om = 6.283 * (coils - 0.5) / length
3366        if not r2:
3367            r2 = r1
3368        pts = []
3369        for t in trange:
3370            f = (length - t) / length
3371            rd = r1 * f + r2 * (1 - f)
3372            pts.append([rd * np.cos(om * t), rd * np.sin(om * t), t])
3373
3374        pts = [[0, 0, 0]] + pts + [[0, 0, length]]
3375        diff = diff / length
3376        theta = np.arccos(diff[2])
3377        phi = np.arctan2(diff[1], diff[0])
3378        sp = Line(pts)
3379        
3380        t = vtki.vtkTransform()
3381        t.Translate(start_pt)
3382        t.RotateZ(np.rad2deg(phi))
3383        t.RotateY(np.rad2deg(theta))
3384
3385        tf = vtki.new("TransformPolyDataFilter")
3386        tf.SetInputData(sp.dataset)
3387        tf.SetTransform(t)
3388        tf.Update()
3389
3390        tuf = vtki.new("TubeFilter")
3391        tuf.SetNumberOfSides(12)
3392        tuf.CappingOn()
3393        tuf.SetInputData(tf.GetOutput())
3394        if not thickness:
3395            thickness = r1 / 10
3396        tuf.SetRadius(thickness)
3397        tuf.Update()
3398
3399        super().__init__(tuf.GetOutput(), c, alpha)
3400
3401        self.phong()
3402        self.base = np.array(start_pt, dtype=float)
3403        self.top  = np.array(end_pt, dtype=float)
3404        self.name = "Spring"
3405
3406
3407class Cylinder(Mesh):
3408    """
3409    Build a cylinder of specified height and radius.
3410    """
3411
3412    def __init__(
3413        self, pos=(0, 0, 0), r=1.0, height=2.0, axis=(0, 0, 1),
3414        cap=True, res=24, c="teal3", alpha=1.0
3415    ) -> None:
3416        """
3417        Build a cylinder of specified height and radius `r`, centered at `pos`.
3418
3419        If `pos` is a list of 2 points, e.g. `pos=[v1, v2]`, build a cylinder with base
3420        centered at `v1` and top at `v2`.
3421
3422        Arguments:
3423            cap : (bool)
3424                enable/disable the caps of the cylinder
3425            res : (int)
3426                resolution of the cylinder sides
3427
3428        ![](https://raw.githubusercontent.com/lorensen/VTKExamples/master/src/Testing/Baseline/Cxx/GeometricObjects/TestCylinder.png)
3429        """
3430        if utils.is_sequence(pos[0]):  # assume user is passing pos=[base, top]
3431            base = np.array(pos[0], dtype=float)
3432            top = np.array(pos[1], dtype=float)
3433            pos = (base + top) / 2
3434            height = np.linalg.norm(top - base)
3435            axis = top - base
3436            axis = utils.versor(axis)
3437        else:
3438            axis = utils.versor(axis)
3439            base = pos - axis * height / 2
3440            top = pos + axis * height / 2
3441
3442        cyl = vtki.new("CylinderSource")
3443        cyl.SetResolution(res)
3444        cyl.SetRadius(r)
3445        cyl.SetHeight(height)
3446        cyl.SetCapping(cap)
3447        cyl.Update()
3448
3449        theta = np.arccos(axis[2])
3450        phi = np.arctan2(axis[1], axis[0])
3451        t = vtki.vtkTransform()
3452        t.PostMultiply()
3453        t.RotateX(90)  # put it along Z
3454        t.RotateY(np.rad2deg(theta))
3455        t.RotateZ(np.rad2deg(phi))
3456        t.Translate(pos)
3457
3458        tf = vtki.new("TransformPolyDataFilter")
3459        tf.SetInputData(cyl.GetOutput())
3460        tf.SetTransform(t)
3461        tf.Update()
3462
3463        super().__init__(tf.GetOutput(), c, alpha)
3464
3465        self.phong()
3466        self.base = base
3467        self.top  = top
3468        self.transform = LinearTransform().translate(pos)
3469        self.name = "Cylinder"
3470
3471
3472class Cone(Mesh):
3473    """Build a cone of specified radius and height."""
3474
3475    def __init__(self, pos=(0, 0, 0), r=1.0, height=3.0, axis=(0, 0, 1),
3476                 res=48, c="green3", alpha=1.0) -> None:
3477        """Build a cone of specified radius `r` and `height`, centered at `pos`."""
3478        con = vtki.new("ConeSource")
3479        con.SetResolution(res)
3480        con.SetRadius(r)
3481        con.SetHeight(height)
3482        con.SetDirection(axis)
3483        con.Update()
3484        super().__init__(con.GetOutput(), c, alpha)
3485        self.phong()
3486        if len(pos) == 2:
3487            pos = (pos[0], pos[1], 0)
3488        self.pos(pos)
3489        v = utils.versor(axis) * height / 2
3490        self.base = pos - v
3491        self.top  = pos + v
3492        self.name = "Cone"
3493
3494
3495class Pyramid(Cone):
3496    """Build a pyramidal shape."""
3497
3498    def __init__(self, pos=(0, 0, 0), s=1.0, height=1.0, axis=(0, 0, 1),
3499                 c="green3", alpha=1) -> None:
3500        """Build a pyramid of specified base size `s` and `height`, centered at `pos`."""
3501        super().__init__(pos, s, height, axis, 4, c, alpha)
3502        self.name = "Pyramid"
3503
3504
3505class Torus(Mesh):
3506    """
3507    Build a toroidal shape.
3508    """
3509
3510    def __init__(self, pos=(0, 0, 0), r1=1.0, r2=0.2, res=36, quads=False, c="yellow3", alpha=1.0) -> None:
3511        """
3512        Build a torus of specified outer radius `r1` internal radius `r2`, centered at `pos`.
3513        If `quad=True` a quad-mesh is generated.
3514        """
3515        if utils.is_sequence(res):
3516            res_u, res_v = res
3517        else:
3518            res_u, res_v = 3 * res, res
3519
3520        if quads:
3521            # https://github.com/marcomusy/vedo/issues/710
3522
3523            n = res_v
3524            m = res_u
3525
3526            theta = np.linspace(0, 2.0 * np.pi, n)
3527            phi = np.linspace(0, 2.0 * np.pi, m)
3528            theta, phi = np.meshgrid(theta, phi)
3529            t = r1 + r2 * np.cos(theta)
3530            x = t * np.cos(phi)
3531            y = t * np.sin(phi)
3532            z = r2 * np.sin(theta)
3533            pts = np.column_stack((x.ravel(), y.ravel(), z.ravel()))
3534
3535            faces = []
3536            for j in range(m - 1):
3537                j1n = (j + 1) * n
3538                for i in range(n - 1):
3539                    faces.append([i + j * n, i + 1 + j * n, i + 1 + j1n, i + j1n])
3540
3541            super().__init__([pts, faces], c, alpha)
3542
3543        else:
3544            rs = vtki.new("ParametricTorus")
3545            rs.SetRingRadius(r1)
3546            rs.SetCrossSectionRadius(r2)
3547            pfs = vtki.new("ParametricFunctionSource")
3548            pfs.SetParametricFunction(rs)
3549            pfs.SetUResolution(res_u)
3550            pfs.SetVResolution(res_v)
3551            pfs.Update()
3552
3553            super().__init__(pfs.GetOutput(), c, alpha)
3554
3555        self.phong()
3556        if len(pos) == 2:
3557            pos = (pos[0], pos[1], 0)
3558        self.pos(pos)
3559        self.name = "Torus"
3560
3561
3562class Paraboloid(Mesh):
3563    """
3564    Build a paraboloid.
3565    """
3566
3567    def __init__(self, pos=(0, 0, 0), height=1.0, res=50, c="cyan5", alpha=1.0) -> None:
3568        """
3569        Build a paraboloid of specified height and radius `r`, centered at `pos`.
3570
3571        Full volumetric expression is:
3572            `F(x,y,z)=a_0x^2+a_1y^2+a_2z^2+a_3xy+a_4yz+a_5xz+ a_6x+a_7y+a_8z+a_9`
3573
3574        ![](https://user-images.githubusercontent.com/32848391/51211547-260ef480-1916-11e9-95f6-4a677e37e355.png)
3575        """
3576        quadric = vtki.new("Quadric")
3577        quadric.SetCoefficients(1, 1, 0, 0, 0, 0, 0, 0, height / 4, 0)
3578        # F(x,y,z) = a0*x^2 + a1*y^2 + a2*z^2
3579        #         + a3*x*y + a4*y*z + a5*x*z
3580        #         + a6*x   + a7*y   + a8*z  +a9
3581        sample = vtki.new("SampleFunction")
3582        sample.SetSampleDimensions(res, res, res)
3583        sample.SetImplicitFunction(quadric)
3584
3585        contours = vtki.new("ContourFilter")
3586        contours.SetInputConnection(sample.GetOutputPort())
3587        contours.GenerateValues(1, 0.01, 0.01)
3588        contours.Update()
3589
3590        super().__init__(contours.GetOutput(), c, alpha)
3591        self.compute_normals().phong()
3592        self.mapper.ScalarVisibilityOff()
3593        self.pos(pos)
3594        self.name = "Paraboloid"
3595
3596
3597class Hyperboloid(Mesh):
3598    """
3599    Build a hyperboloid.
3600    """
3601
3602    def __init__(self, pos=(0, 0, 0), a2=1.0, value=0.5, res=100, c="pink4", alpha=1.0) -> None:
3603        """
3604        Build a hyperboloid of specified aperture `a2` and `height`, centered at `pos`.
3605
3606        Full volumetric expression is:
3607            `F(x,y,z)=a_0x^2+a_1y^2+a_2z^2+a_3xy+a_4yz+a_5xz+ a_6x+a_7y+a_8z+a_9`
3608        """
3609        q = vtki.new("Quadric")
3610        q.SetCoefficients(2, 2, -1 / a2, 0, 0, 0, 0, 0, 0, 0)
3611        # F(x,y,z) = a0*x^2 + a1*y^2 + a2*z^2
3612        #         + a3*x*y + a4*y*z + a5*x*z
3613        #         + a6*x   + a7*y   + a8*z  +a9
3614        sample = vtki.new("SampleFunction")
3615        sample.SetSampleDimensions(res, res, res)
3616        sample.SetImplicitFunction(q)
3617
3618        contours = vtki.new("ContourFilter")
3619        contours.SetInputConnection(sample.GetOutputPort())
3620        contours.GenerateValues(1, value, value)
3621        contours.Update()
3622
3623        super().__init__(contours.GetOutput(), c, alpha)
3624        self.compute_normals().phong()
3625        self.mapper.ScalarVisibilityOff()
3626        self.pos(pos)
3627        self.name = "Hyperboloid"
3628
3629
3630def Marker(symbol, pos=(0, 0, 0), c="k", alpha=1.0, s=0.1, filled=True) -> Any:
3631    """
3632    Generate a marker shape. Typically used in association with `Glyph`.
3633    """
3634    if isinstance(symbol, Mesh):
3635        return symbol.c(c).alpha(alpha).lighting("off")
3636
3637    if isinstance(symbol, int):
3638        symbs = [".", "o", "O", "0", "p", "*", "h", "D", "d", "v", "^", ">", "<", "s", "x", "a"]
3639        symbol = symbol % len(symbs)
3640        symbol = symbs[symbol]
3641
3642    if symbol == ".":
3643        mesh = Polygon(nsides=24, r=s * 0.6)
3644    elif symbol == "o":
3645        mesh = Polygon(nsides=24, r=s * 0.75)
3646    elif symbol == "O":
3647        mesh = Disc(r1=s * 0.6, r2=s * 0.75, res=(1, 24))
3648    elif symbol == "0":
3649        m1 = Disc(r1=s * 0.6, r2=s * 0.75, res=(1, 24))
3650        m2 = Circle(r=s * 0.36).reverse()
3651        mesh = merge(m1, m2)
3652    elif symbol == "p":
3653        mesh = Polygon(nsides=5, r=s)
3654    elif symbol == "*":
3655        mesh = Star(r1=0.65 * s * 1.1, r2=s * 1.1, line=not filled)
3656    elif symbol == "h":
3657        mesh = Polygon(nsides=6, r=s)
3658    elif symbol == "D":
3659        mesh = Polygon(nsides=4, r=s)
3660    elif symbol == "d":
3661        mesh = Polygon(nsides=4, r=s * 1.1).scale([0.5, 1, 1])
3662    elif symbol == "v":
3663        mesh = Polygon(nsides=3, r=s).rotate_z(180)
3664    elif symbol == "^":
3665        mesh = Polygon(nsides=3, r=s)
3666    elif symbol == ">":
3667        mesh = Polygon(nsides=3, r=s).rotate_z(-90)
3668    elif symbol == "<":
3669        mesh = Polygon(nsides=3, r=s).rotate_z(90)
3670    elif symbol == "s":
3671        mesh = Mesh(
3672            [[[-1, -1, 0], [1, -1, 0], [1, 1, 0], [-1, 1, 0]], [[0, 1, 2, 3]]]
3673        ).scale(s / 1.4)
3674    elif symbol == "x":
3675        mesh = Text3D("+", pos=(0, 0, 0), s=s * 2.6, justify="center", depth=0)
3676        # mesh.rotate_z(45)
3677    elif symbol == "a":
3678        mesh = Text3D("*", pos=(0, 0, 0), s=s * 2.6, justify="center", depth=0)
3679    else:
3680        mesh = Text3D(symbol, pos=(0, 0, 0), s=s * 2, justify="center", depth=0)
3681    mesh.flat().lighting("off").wireframe(not filled).c(c).alpha(alpha)
3682    if len(pos) == 2:
3683        pos = (pos[0], pos[1], 0)
3684    mesh.pos(pos)
3685    mesh.name = "Marker"
3686    return mesh
3687
3688
3689class Brace(Mesh):
3690    """
3691    Create a brace (bracket) shape.
3692    """
3693
3694    def __init__(
3695        self,
3696        q1,
3697        q2,
3698        style="}",
3699        padding1=0.0,
3700        font="Theemim",
3701        comment="",
3702        justify=None,
3703        angle=0.0,
3704        padding2=0.2,
3705        s=1.0,
3706        italic=0,
3707        c="k1",
3708        alpha=1.0,
3709    ) -> None:
3710        """
3711        Create a brace (bracket) shape which spans from point q1 to point q2.
3712
3713        Arguments:
3714            q1 : (list)
3715                point 1.
3716            q2 : (list)
3717                point 2.
3718            style : (str)
3719                style of the bracket, eg. `{}, [], (), <>`.
3720            padding1 : (float)
3721                padding space in percent form the input points.
3722            font : (str)
3723                font type
3724            comment : (str)
3725                additional text to appear next to the brace symbol.
3726            justify : (str)
3727                specify the anchor point to justify text comment, e.g. "top-left".
3728            italic : float
3729                italicness of the text comment (can be a positive or negative number)
3730            angle : (float)
3731                rotation angle of text. Use `None` to keep it horizontal.
3732            padding2 : (float)
3733                padding space in percent form brace to text comment.
3734            s : (float)
3735                scale factor for the comment
3736
3737        Examples:
3738            - [scatter3.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/scatter3.py)
3739
3740                ![](https://vedo.embl.es/images/pyplot/scatter3.png)
3741        """
3742        if isinstance(q1, vtki.vtkActor):
3743            q1 = q1.GetPosition()
3744        if isinstance(q2, vtki.vtkActor):
3745            q2 = q2.GetPosition()
3746        if len(q1) == 2:
3747            q1 = [q1[0], q1[1], 0.0]
3748        if len(q2) == 2:
3749            q2 = [q2[0], q2[1], 0.0]
3750        q1 = np.array(q1, dtype=float)
3751        q2 = np.array(q2, dtype=float)
3752        mq = (q1 + q2) / 2
3753        q1 = q1 - mq
3754        q2 = q2 - mq
3755        d = np.linalg.norm(q2 - q1)
3756        q2[2] = q1[2]
3757
3758        if style not in "{}[]()<>|I":
3759            vedo.logger.error(f"unknown style {style}." + "Use {}[]()<>|I")
3760            style = "}"
3761
3762        flip = False
3763        if style in ["{", "[", "(", "<"]:
3764            flip = True
3765            i = ["{", "[", "(", "<"].index(style)
3766            style = ["}", "]", ")", ">"][i]
3767
3768        br = Text3D(style, font="Theemim", justify="center-left")
3769        br.scale([0.4, 1, 1])
3770
3771        angler = np.arctan2(q2[1], q2[0]) * 180 / np.pi - 90
3772        if flip:
3773            angler += 180
3774
3775        _, x1, y0, y1, _, _ = br.bounds()
3776        if comment:
3777            just = "center-top"
3778            if angle is None:
3779                angle = -angler + 90
3780                if not flip:
3781                    angle += 180
3782
3783            if flip:
3784                angle += 180
3785                just = "center-bottom"
3786            if justify is not None:
3787                just = justify
3788            cmt = Text3D(comment, font=font, justify=just, italic=italic)
3789            cx0, cx1 = cmt.xbounds()
3790            cmt.rotate_z(90 + angle)
3791            cmt.scale(1 / (cx1 - cx0) * s * len(comment) / 5)
3792            cmt.shift(x1 * (1 + padding2), 0, 0)
3793            poly = merge(br, cmt).dataset
3794
3795        else:
3796            poly = br.dataset
3797
3798        tr = vtki.vtkTransform()
3799        tr.Translate(mq)
3800        tr.RotateZ(angler)
3801        tr.Translate(padding1 * d, 0, 0)
3802        pscale = 1
3803        tr.Scale(pscale / (y1 - y0) * d, pscale / (y1 - y0) * d, 1)
3804
3805        tf = vtki.new("TransformPolyDataFilter")
3806        tf.SetInputData(poly)
3807        tf.SetTransform(tr)
3808        tf.Update()
3809        poly = tf.GetOutput()
3810
3811        super().__init__(poly, c, alpha)
3812
3813        self.base = q1
3814        self.top  = q2
3815        self.name = "Brace"
3816
3817
3818class Star3D(Mesh):
3819    """
3820    Build a 3D starred shape.
3821    """
3822
3823    def __init__(self, pos=(0, 0, 0), r=1.0, thickness=0.1, c="blue4", alpha=1.0) -> None:
3824        """
3825        Build a 3D star shape of 5 cusps, mainly useful as a 3D marker.
3826        """
3827        pts = ((1.34, 0., -0.37), (5.75e-3, -0.588, thickness/10), (0.377, 0.,-0.38),
3828               (0.0116, 0., -1.35), (-0.366, 0., -0.384), (-1.33, 0., -0.385),
3829               (-0.600, 0., 0.321), (-0.829, 0., 1.19), (-1.17e-3, 0., 0.761),
3830               (0.824, 0., 1.20), (0.602, 0., 0.328), (6.07e-3, 0.588, thickness/10))
3831        fcs = [[0, 1, 2], [0, 11,10], [2, 1, 3], [2, 11, 0], [3, 1, 4], [3, 11, 2],
3832               [4, 1, 5], [4, 11, 3], [5, 1, 6], [5, 11, 4], [6, 1, 7], [6, 11, 5],
3833               [7, 1, 8], [7, 11, 6], [8, 1, 9], [8, 11, 7], [9, 1,10], [9, 11, 8],
3834               [10,1, 0],[10,11, 9]]
3835
3836        super().__init__([pts, fcs], c, alpha)
3837        self.rotate_x(90)
3838        self.scale(r).lighting("shiny")
3839
3840        if len(pos) == 2:
3841            pos = (pos[0], pos[1], 0)
3842        self.pos(pos)
3843        self.name = "Star3D"
3844
3845
3846class Cross3D(Mesh):
3847    """
3848    Build a 3D cross shape.
3849    """
3850
3851    def __init__(self, pos=(0, 0, 0), s=1.0, thickness=0.3, c="b", alpha=1.0) -> None:
3852        """
3853        Build a 3D cross shape, mainly useful as a 3D marker.
3854        """
3855        if len(pos) == 2:
3856            pos = (pos[0], pos[1], 0)
3857
3858        c1 = Cylinder(r=thickness * s, height=2 * s)
3859        c2 = Cylinder(r=thickness * s, height=2 * s).rotate_x(90)
3860        c3 = Cylinder(r=thickness * s, height=2 * s).rotate_y(90)
3861        poly = merge(c1, c2, c3).color(c).alpha(alpha).pos(pos).dataset
3862        super().__init__(poly, c, alpha)
3863        self.name = "Cross3D"
3864
3865
3866class ParametricShape(Mesh):
3867    """
3868    A set of built-in shapes mainly for illustration purposes.
3869    """
3870
3871    def __init__(self, name, res=51, n=25, seed=1):
3872        """
3873        A set of built-in shapes mainly for illustration purposes.
3874
3875        Name can be an integer or a string in this list:
3876            `['Boy', 'ConicSpiral', 'CrossCap', 'Dini', 'Enneper',
3877            'Figure8Klein', 'Klein', 'Mobius', 'RandomHills', 'Roman',
3878            'SuperEllipsoid', 'BohemianDome', 'Bour', 'CatalanMinimal',
3879            'Henneberg', 'Kuen', 'PluckerConoid', 'Pseudosphere']`.
3880
3881        Example:
3882            ```python
3883            from vedo import *
3884            settings.immediate_rendering = False
3885            plt = Plotter(N=18)
3886            for i in range(18):
3887                ps = ParametricShape(i).color(i)
3888                plt.at(i).show(ps, ps.name)
3889            plt.interactive().close()
3890            ```
3891            <img src="https://user-images.githubusercontent.com/32848391/69181075-bb6aae80-0b0e-11ea-92f7-d0cd3b9087bf.png" width="700">
3892        """
3893
3894        shapes = [
3895            "Boy",
3896            "ConicSpiral",
3897            "CrossCap",
3898            "Enneper",
3899            "Figure8Klein",
3900            "Klein",
3901            "Dini",
3902            "Mobius",
3903            "RandomHills",
3904            "Roman",
3905            "SuperEllipsoid",
3906            "BohemianDome",
3907            "Bour",
3908            "CatalanMinimal",
3909            "Henneberg",
3910            "Kuen",
3911            "PluckerConoid",
3912            "Pseudosphere",
3913        ]
3914
3915        if isinstance(name, int):
3916            name = name % len(shapes)
3917            name = shapes[name]
3918
3919        if name == "Boy":
3920            ps = vtki.new("ParametricBoy")
3921        elif name == "ConicSpiral":
3922            ps = vtki.new("ParametricConicSpiral")
3923        elif name == "CrossCap":
3924            ps = vtki.new("ParametricCrossCap")
3925        elif name == "Dini":
3926            ps = vtki.new("ParametricDini")
3927        elif name == "Enneper":
3928            ps = vtki.new("ParametricEnneper")
3929        elif name == "Figure8Klein":
3930            ps = vtki.new("ParametricFigure8Klein")
3931        elif name == "Klein":
3932            ps = vtki.new("ParametricKlein")
3933        elif name == "Mobius":
3934            ps = vtki.new("ParametricMobius")
3935            ps.SetRadius(2.0)
3936            ps.SetMinimumV(-0.5)
3937            ps.SetMaximumV(0.5)
3938        elif name == "RandomHills":
3939            ps = vtki.new("ParametricRandomHills")
3940            ps.AllowRandomGenerationOn()
3941            ps.SetRandomSeed(seed)
3942            ps.SetNumberOfHills(n)
3943        elif name == "Roman":
3944            ps = vtki.new("ParametricRoman")
3945        elif name == "SuperEllipsoid":
3946            ps = vtki.new("ParametricSuperEllipsoid")
3947            ps.SetN1(0.5)
3948            ps.SetN2(0.4)
3949        elif name == "BohemianDome":
3950            ps = vtki.new("ParametricBohemianDome")
3951            ps.SetA(5.0)
3952            ps.SetB(1.0)
3953            ps.SetC(2.0)
3954        elif name == "Bour":
3955            ps = vtki.new("ParametricBour")
3956        elif name == "CatalanMinimal":
3957            ps = vtki.new("ParametricCatalanMinimal")
3958        elif name == "Henneberg":
3959            ps = vtki.new("ParametricHenneberg")
3960        elif name == "Kuen":
3961            ps = vtki.new("ParametricKuen")
3962            ps.SetDeltaV0(0.001)
3963        elif name == "PluckerConoid":
3964            ps = vtki.new("ParametricPluckerConoid")
3965        elif name == "Pseudosphere":
3966            ps = vtki.new("ParametricPseudosphere")
3967        else:
3968            vedo.logger.error(f"unknown ParametricShape {name}")
3969            return
3970
3971        pfs = vtki.new("ParametricFunctionSource")
3972        pfs.SetParametricFunction(ps)
3973        pfs.SetUResolution(res)
3974        pfs.SetVResolution(res)
3975        pfs.SetWResolution(res)
3976        pfs.SetScalarModeToZ()
3977        pfs.Update()
3978
3979        super().__init__(pfs.GetOutput())
3980
3981        if name == "RandomHills": self.shift([0,-10,-2.25])
3982        if name != 'Kuen': self.normalize()
3983        if name == 'Dini': self.scale(0.4)
3984        if name == 'Enneper': self.scale(0.4)
3985        if name == 'ConicSpiral': self.bc('tomato')
3986        self.name = name
3987
3988
3989@lru_cache(None)
3990def _load_font(font) -> np.ndarray:
3991    # print('_load_font()', font)
3992
3993    if utils.is_number(font):
3994        font = list(settings.font_parameters.keys())[int(font)]
3995
3996    if font.endswith(".npz"):  # user passed font as a local path
3997        fontfile = font
3998        font = os.path.basename(font).split(".")[0]
3999
4000    elif font.startswith("https"):  # user passed URL link, make it a path
4001        try:
4002            fontfile = vedo.file_io.download(font, verbose=False, force=False)
4003            font = os.path.basename(font).split(".")[0]
4004        except:
4005            vedo.logger.warning(f"font {font} not found")
4006            font = settings.default_font
4007            fontfile = os.path.join(vedo.fonts_path, font + ".npz")
4008
4009    else:  # user passed font by its standard name
4010        font = font[:1].upper() + font[1:]  # capitalize first letter only
4011        fontfile = os.path.join(vedo.fonts_path, font + ".npz")
4012
4013        if font not in settings.font_parameters.keys():
4014            font = "Normografo"
4015            vedo.logger.warning(
4016                f"Unknown font: {font}\n"
4017                f"Available 3D fonts are: "
4018                f"{list(settings.font_parameters.keys())}\n"
4019                f"Using font {font} instead."
4020            )
4021            fontfile = os.path.join(vedo.fonts_path, font + ".npz")
4022
4023        if not settings.font_parameters[font]["islocal"]:
4024            font = "https://vedo.embl.es/fonts/" + font + ".npz"
4025            try:
4026                fontfile = vedo.file_io.download(font, verbose=False, force=False)
4027                font = os.path.basename(font).split(".")[0]
4028            except:
4029                vedo.logger.warning(f"font {font} not found")
4030                font = settings.default_font
4031                fontfile = os.path.join(vedo.fonts_path, font + ".npz")
4032
4033    #####
4034    try:
4035        font_meshes = np.load(fontfile, allow_pickle=True)["font"][0]
4036    except:
4037        vedo.logger.warning(f"font name {font} not found.")
4038        raise RuntimeError
4039    return font_meshes
4040
4041
4042@lru_cache(None)
4043def _get_font_letter(font, letter):
4044    # print("_get_font_letter", font, letter)
4045    font_meshes = _load_font(font)
4046    try:
4047        pts, faces = font_meshes[letter]
4048        return utils.buildPolyData(pts.astype(float), faces)
4049    except KeyError:
4050        return None
4051
4052
4053class Text3D(Mesh):
4054    """
4055    Generate a 3D polygonal Mesh to represent a text string.
4056    """
4057
4058    def __init__(
4059        self,
4060        txt,
4061        pos=(0, 0, 0),
4062        s=1.0,
4063        font="",
4064        hspacing=1.15,
4065        vspacing=2.15,
4066        depth=0.0,
4067        italic=False,
4068        justify="bottom-left",
4069        literal=False,
4070        c=None,
4071        alpha=1.0,
4072    ) -> None:
4073        """
4074        Generate a 3D polygonal `Mesh` representing a text string.
4075
4076        Can render strings like `3.7 10^9` or `H_2 O` with subscripts and superscripts.
4077        Most Latex symbols are also supported.
4078
4079        Symbols `~ ^ _` are reserved modifiers:
4080        - use ~ to add a short space, 1/4 of the default empty space,
4081        - use ^ and _ to start up/sub scripting, a space terminates their effect.
4082
4083        Monospaced fonts are: `Calco, ComicMono, Glasgo, SmartCouric, VictorMono, Justino`.
4084
4085        More fonts at: https://vedo.embl.es/fonts/
4086
4087        Arguments:
4088            pos : (list)
4089                position coordinates in 3D space
4090            s : (float)
4091                vertical size of the text (as scaling factor)
4092            depth : (float)
4093                text thickness (along z)
4094            italic : (bool), float
4095                italic font type (can be a signed float too)
4096            justify : (str)
4097                text justification as centering of the bounding box
4098                (bottom-left, bottom-right, top-left, top-right, centered)
4099            font : (str, int)
4100                some of the available 3D-polygonized fonts are:
4101                Bongas, Calco, Comae, ComicMono, Kanopus, Glasgo, Ubuntu,
4102                LogoType, Normografo, Quikhand, SmartCouric, Theemim, VictorMono, VTK,
4103                Capsmall, Cartoons123, Vega, Justino, Spears, Meson.
4104
4105                Check for more at https://vedo.embl.es/fonts/
4106
4107                Or type in your terminal `vedo --run fonts`.
4108
4109                Default is Normografo, which can be changed using `settings.default_font`.
4110
4111            hspacing : (float)
4112                horizontal spacing of the font
4113            vspacing : (float)
4114                vertical spacing of the font for multiple lines text
4115            literal : (bool)
4116                if set to True will ignore modifiers like _ or ^
4117
4118        Examples:
4119            - [markpoint.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/markpoint.py)
4120            - [fonts.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/fonts.py)
4121            - [caption.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/caption.py)
4122
4123            ![](https://vedo.embl.es/images/pyplot/fonts3d.png)
4124
4125        .. note:: Type `vedo -r fonts` for a demo.
4126        """
4127        if len(pos) == 2:
4128            pos = (pos[0], pos[1], 0)
4129
4130        if c is None:  # automatic black or white
4131            pli = vedo.plotter_instance
4132            if pli and pli.renderer:
4133                c = (0.9, 0.9, 0.9)
4134                if pli.renderer.GetGradientBackground():
4135                    bgcol = pli.renderer.GetBackground2()
4136                else:
4137                    bgcol = pli.renderer.GetBackground()
4138                if np.sum(bgcol) > 1.5:
4139                    c = (0.1, 0.1, 0.1)
4140            else:
4141                c = (0.6, 0.6, 0.6)
4142
4143        tpoly = self._get_text3d_poly(
4144            txt, s, font, hspacing, vspacing, depth, italic, justify, literal
4145        )
4146
4147        super().__init__(tpoly, c, alpha)
4148
4149        self.pos(pos)
4150        self.lighting("off")
4151
4152        self.actor.PickableOff()
4153        self.actor.DragableOff()
4154        self.init_scale = s
4155        self.name = "Text3D"
4156        self.txt = txt
4157        self.justify = justify
4158
4159    def text(
4160        self,
4161        txt=None,
4162        s=1,
4163        font="",
4164        hspacing=1.15,
4165        vspacing=2.15,
4166        depth=0,
4167        italic=False,
4168        justify="",
4169        literal=False,
4170    ) -> "Text3D":
4171        """
4172        Update the text and some of its properties.
4173
4174        Check [available fonts here](https://vedo.embl.es/fonts).
4175        """
4176        if txt is None:
4177            return self.txt
4178        if not justify:
4179            justify = self.justify
4180
4181        poly = self._get_text3d_poly(
4182            txt, self.init_scale * s, font, hspacing, vspacing,
4183            depth, italic, justify, literal
4184        )
4185
4186        # apply the current transformation to the new polydata
4187        tf = vtki.new("TransformPolyDataFilter")
4188        tf.SetInputData(poly)
4189        tf.SetTransform(self.transform.T)
4190        tf.Update()
4191        tpoly = tf.GetOutput()
4192
4193        self._update(tpoly)
4194        self.txt = txt
4195        return self
4196
4197    def _get_text3d_poly(
4198        self,
4199        txt,
4200        s=1,
4201        font="",
4202        hspacing=1.15,
4203        vspacing=2.15,
4204        depth=0,
4205        italic=False,
4206        justify="bottom-left",
4207        literal=False,
4208    ) -> vtki.vtkPolyData:
4209        if not font:
4210            font = settings.default_font
4211
4212        txt = str(txt)
4213
4214        if font == "VTK":  #######################################
4215            vtt = vtki.new("VectorText")
4216            vtt.SetText(txt)
4217            vtt.Update()
4218            tpoly = vtt.GetOutput()
4219
4220        else:  ###################################################
4221
4222            stxt = set(txt)  # check here if null or only spaces
4223            if not txt or (len(stxt) == 1 and " " in stxt):
4224                return vtki.vtkPolyData()
4225
4226            if italic is True:
4227                italic = 1
4228
4229            if isinstance(font, int):
4230                lfonts = list(settings.font_parameters.keys())
4231                font = font % len(lfonts)
4232                font = lfonts[font]
4233
4234            if font not in settings.font_parameters.keys():
4235                fpars = settings.font_parameters["Normografo"]
4236            else:
4237                fpars = settings.font_parameters[font]
4238
4239            # ad hoc adjustments
4240            mono = fpars["mono"]
4241            lspacing = fpars["lspacing"]
4242            hspacing *= fpars["hspacing"]
4243            fscale = fpars["fscale"]
4244            dotsep = fpars["dotsep"]
4245
4246            # replacements
4247            if ":" in txt:
4248                for r in _reps:
4249                    txt = txt.replace(r[0], r[1])
4250
4251            if not literal:
4252                reps2 = [
4253                    (r"\_", "┭"),  # trick to protect ~ _ and ^ chars
4254                    (r"\^", "┮"),  #
4255                    (r"\~", "┯"),  #
4256                    ("**", "^"),  # order matters
4257                    ("e+0", dotsep + "10^"),
4258                    ("e-0", dotsep + "10^-"),
4259                    ("E+0", dotsep + "10^"),
4260                    ("E-0", dotsep + "10^-"),
4261                    ("e+", dotsep + "10^"),
4262                    ("e-", dotsep + "10^-"),
4263                    ("E+", dotsep + "10^"),
4264                    ("E-", dotsep + "10^-"),
4265                ]
4266                for r in reps2:
4267                    txt = txt.replace(r[0], r[1])
4268
4269            xmax, ymax, yshift, scale = 0.0, 0.0, 0.0, 1.0
4270            save_xmax = 0.0
4271
4272            notfounds = set()
4273            polyletters = []
4274            ntxt = len(txt)
4275            for i, t in enumerate(txt):
4276                ##########
4277                if t == "┭":
4278                    t = "_"
4279                elif t == "┮":
4280                    t = "^"
4281                elif t == "┯":
4282                    t = "~"
4283                elif t == "^" and not literal:
4284                    if yshift < 0:
4285                        xmax = save_xmax
4286                    yshift = 0.9 * fscale
4287                    scale = 0.5
4288                    continue
4289                elif t == "_" and not literal:
4290                    if yshift > 0:
4291                        xmax = save_xmax
4292                    yshift = -0.3 * fscale
4293                    scale = 0.5
4294                    continue
4295                elif (t in (" ", "\\n")) and yshift:
4296                    yshift = 0.0
4297                    scale = 1.0
4298                    save_xmax = xmax
4299                    if t == " ":
4300                        continue
4301                elif t == "~":