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        Examples:
1478            - [ribbon.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/ribbon.py)
1479            - [tube_radii.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/tube_radii.py)
1480
1481                ![](https://vedo.embl.es/images/basic/tube.png)
1482        """
1483        if utils.is_sequence(points):
1484            vpoints = vtki.vtkPoints()
1485            idx = len(points)
1486            for p in points:
1487                vpoints.InsertNextPoint(p)
1488            line = vtki.new("PolyLine")
1489            line.GetPointIds().SetNumberOfIds(idx)
1490            for i in range(idx):
1491                line.GetPointIds().SetId(i, i)
1492            lines = vtki.vtkCellArray()
1493            lines.InsertNextCell(line)
1494            polyln = vtki.vtkPolyData()
1495            polyln.SetPoints(vpoints)
1496            polyln.SetLines(lines)            
1497            self.base = np.asarray(points[0], dtype=float)
1498            self.top = np.asarray(points[-1], dtype=float)
1499
1500        elif isinstance(points, Mesh):
1501            polyln = points.dataset
1502            n = polyln.GetNumberOfPoints()
1503            self.base = np.array(polyln.GetPoint(0))
1504            self.top = np.array(polyln.GetPoint(n - 1))
1505
1506        # from vtkmodules.vtkFiltersCore import vtkTubeBender
1507        # bender = vtkTubeBender()
1508        # bender.SetInputData(polyln)
1509        # bender.SetRadius(r)
1510        # bender.Update()
1511        # polyln = bender.GetOutput()
1512
1513        tuf = vtki.new("TubeFilter")
1514        tuf.SetCapping(cap)
1515        tuf.SetNumberOfSides(res)
1516        tuf.SetInputData(polyln)
1517        if utils.is_sequence(r):
1518            arr = utils.numpy2vtk(r, dtype=float)
1519            arr.SetName("TubeRadius")
1520            polyln.GetPointData().AddArray(arr)
1521            polyln.GetPointData().SetActiveScalars("TubeRadius")
1522            tuf.SetVaryRadiusToVaryRadiusByAbsoluteScalar()
1523        else:
1524            tuf.SetRadius(r)
1525
1526        usingColScals = False
1527        if utils.is_sequence(c):
1528            usingColScals = True
1529            cc = vtki.vtkUnsignedCharArray()
1530            cc.SetName("TubeColors")
1531            cc.SetNumberOfComponents(3)
1532            cc.SetNumberOfTuples(len(c))
1533            for i, ic in enumerate(c):
1534                r, g, b = get_color(ic)
1535                cc.InsertTuple3(i, int(255 * r), int(255 * g), int(255 * b))
1536            polyln.GetPointData().AddArray(cc)
1537            c = None
1538        tuf.Update()
1539
1540        super().__init__(tuf.GetOutput(), c, alpha)
1541        self.phong()
1542        if usingColScals:
1543            self.mapper.SetScalarModeToUsePointFieldData()
1544            self.mapper.ScalarVisibilityOn()
1545            self.mapper.SelectColorArray("TubeColors")
1546            self.mapper.Modified()
1547        self.name = "Tube"
1548
1549
1550def ThickTube(pts, r1, r2, res=12, c=None, alpha=1.0) -> Union["Mesh", None]:
1551    """
1552    Create a tube with a thickness along a line of points.
1553
1554    Example:
1555    ```python
1556    from vedo import *
1557    pts = [[sin(x), cos(x), x/3] for x in np.arange(0.1, 3, 0.3)]
1558    vline = Line(pts, lw=5, c='red5')
1559    thick_tube = ThickTube(vline, r1=0.2, r2=0.3).lw(1)
1560    show(vline, thick_tube, axes=1).close()
1561    ```
1562    ![](https://vedo.embl.es/images/feats/thick_tube.png)
1563    """
1564
1565    def make_cap(t1, t2):
1566        newpoints = t1.vertices.tolist() + t2.vertices.tolist()
1567        newfaces = []
1568        for i in range(n - 1):
1569            newfaces.append([i, i + 1, i + n])
1570            newfaces.append([i + n, i + 1, i + n + 1])
1571        newfaces.append([2 * n - 1, 0, n])
1572        newfaces.append([2 * n - 1, n - 1, 0])
1573        capm = utils.buildPolyData(newpoints, newfaces)
1574        return capm
1575
1576    assert r1 < r2
1577
1578    t1 = Tube(pts, r=r1, cap=False, res=res)
1579    t2 = Tube(pts, r=r2, cap=False, res=res)
1580
1581    tc1a, tc1b = t1.boundaries().split()
1582    tc2a, tc2b = t2.boundaries().split()
1583    n = tc1b.npoints
1584
1585    tc1b.join(reset=True).clean()  # needed because indices are flipped
1586    tc2b.join(reset=True).clean()
1587
1588    capa = make_cap(tc1a, tc2a)
1589    capb = make_cap(tc1b, tc2b)
1590
1591    thick_tube = merge(t1, t2, capa, capb)
1592    if thick_tube:
1593        thick_tube.c(c).alpha(alpha)
1594        thick_tube.base = t1.base
1595        thick_tube.top  = t1.top
1596        thick_tube.name = "ThickTube"
1597        return thick_tube
1598    return None
1599
1600
1601class Tubes(Mesh):
1602    """
1603    Build tubes around a `Lines` object.
1604    """
1605    def __init__(
1606            self,
1607            lines,
1608            r=1,
1609            vary_radius_by_scalar=False,
1610            vary_radius_by_vector=False,
1611            vary_radius_by_vector_norm=False,
1612            vary_radius_by_absolute_scalar=False,
1613            max_radius_factor=100,
1614            cap=True,
1615            res=12
1616        ) -> None:
1617        """
1618        Wrap tubes around the input `Lines` object.
1619
1620        Arguments:
1621            lines : (Lines)
1622                input Lines object.
1623            r : (float)
1624                constant radius
1625            vary_radius_by_scalar : (bool)
1626                use scalar array to control radius
1627            vary_radius_by_vector : (bool)
1628                use vector array to control radius
1629            vary_radius_by_vector_norm : (bool)
1630                use vector norm to control radius
1631            vary_radius_by_absolute_scalar : (bool)
1632                use absolute scalar value to control radius
1633            max_radius_factor : (float)
1634                max tube radius as a multiple of the min radius
1635            cap : (bool)
1636                capping of the tube
1637            res : (int)
1638                resolution, number of the sides of the tube
1639            c : (color)
1640                constant color or list of colors for each point.
1641        
1642        Examples:
1643            - [streamlines1.py](https://github.com/marcomusy/vedo/blob/master/examples/volumetric/streamlines1.py)
1644        """
1645        plines = lines.dataset
1646        if plines.GetNumberOfLines() == 0:
1647            vedo.logger.warning("Tubes(): input Lines is empty.")
1648
1649        tuf = vtki.new("TubeFilter")
1650        if vary_radius_by_scalar:
1651            tuf.SetVaryRadiusToVaryRadiusByScalar()
1652        elif vary_radius_by_vector:
1653            tuf.SetVaryRadiusToVaryRadiusByVector()
1654        elif vary_radius_by_vector_norm:
1655            tuf.SetVaryRadiusToVaryRadiusByVectorNorm()
1656        elif vary_radius_by_absolute_scalar:
1657            tuf.SetVaryRadiusToVaryRadiusByAbsoluteScalar()
1658        tuf.SetRadius(r)
1659        tuf.SetCapping(cap)
1660        tuf.SetGenerateTCoords(0)
1661        tuf.SetSidesShareVertices(1)
1662        tuf.SetRadiusFactor(max_radius_factor)
1663        tuf.SetNumberOfSides(res)
1664        tuf.SetInputData(plines)
1665        tuf.Update()
1666
1667        super().__init__(tuf.GetOutput())
1668        self.name = "Tubes"
1669    
1670
1671class Ribbon(Mesh):
1672    """
1673    Connect two lines to generate the surface inbetween.
1674    Set the mode by which to create the ruled surface.
1675
1676    It also works with a single line in input. In this case the ribbon
1677    is formed by following the local plane of the line in space.
1678    """
1679
1680    def __init__(
1681        self,
1682        line1,
1683        line2=None,
1684        mode=0,
1685        closed=False,
1686        width=None,
1687        res=(200, 5),
1688        c="indigo3",
1689        alpha=1.0,
1690    ) -> None:
1691        """
1692        Arguments:
1693            mode : (int)
1694                If mode=0, resample evenly the input lines (based on length)
1695                and generates triangle strips.
1696
1697                If mode=1, use the existing points and walks around the
1698                polyline using existing points.
1699
1700            closed : (bool)
1701                if True, join the last point with the first to form a closed surface
1702
1703            res : (list)
1704                ribbon resolutions along the line and perpendicularly to it.
1705
1706        Examples:
1707            - [ribbon.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/ribbon.py)
1708
1709                ![](https://vedo.embl.es/images/basic/ribbon.png)
1710        """
1711
1712        if isinstance(line1, Points):
1713            line1 = line1.vertices
1714
1715        if isinstance(line2, Points):
1716            line2 = line2.vertices
1717
1718        elif line2 is None:
1719            #############################################
1720            ribbon_filter = vtki.new("RibbonFilter")
1721            aline = Line(line1)
1722            ribbon_filter.SetInputData(aline.dataset)
1723            if width is None:
1724                width = aline.diagonal_size() / 20.0
1725            ribbon_filter.SetWidth(width)
1726            ribbon_filter.Update()
1727            # convert triangle strips to polygons
1728            tris = vtki.new("TriangleFilter")
1729            tris.SetInputData(ribbon_filter.GetOutput())
1730            tris.Update()
1731
1732            super().__init__(tris.GetOutput(), c, alpha)
1733            self.name = "Ribbon"
1734            ##############################################
1735            return  ######################################
1736            ##############################################
1737
1738        line1 = np.asarray(line1)
1739        line2 = np.asarray(line2)
1740
1741        if closed:
1742            line1 = line1.tolist()
1743            line1 += [line1[0]]
1744            line2 = line2.tolist()
1745            line2 += [line2[0]]
1746            line1 = np.array(line1)
1747            line2 = np.array(line2)
1748
1749        if len(line1[0]) == 2:
1750            line1 = np.c_[line1, np.zeros(len(line1))]
1751        if len(line2[0]) == 2:
1752            line2 = np.c_[line2, np.zeros(len(line2))]
1753
1754        ppoints1 = vtki.vtkPoints()  # Generate the polyline1
1755        ppoints1.SetData(utils.numpy2vtk(line1, dtype=np.float32))
1756        lines1 = vtki.vtkCellArray()
1757        lines1.InsertNextCell(len(line1))
1758        for i in range(len(line1)):
1759            lines1.InsertCellPoint(i)
1760        poly1 = vtki.vtkPolyData()
1761        poly1.SetPoints(ppoints1)
1762        poly1.SetLines(lines1)
1763
1764        ppoints2 = vtki.vtkPoints()  # Generate the polyline2
1765        ppoints2.SetData(utils.numpy2vtk(line2, dtype=np.float32))
1766        lines2 = vtki.vtkCellArray()
1767        lines2.InsertNextCell(len(line2))
1768        for i in range(len(line2)):
1769            lines2.InsertCellPoint(i)
1770        poly2 = vtki.vtkPolyData()
1771        poly2.SetPoints(ppoints2)
1772        poly2.SetLines(lines2)
1773
1774        # build the lines
1775        lines1 = vtki.vtkCellArray()
1776        lines1.InsertNextCell(poly1.GetNumberOfPoints())
1777        for i in range(poly1.GetNumberOfPoints()):
1778            lines1.InsertCellPoint(i)
1779
1780        polygon1 = vtki.vtkPolyData()
1781        polygon1.SetPoints(ppoints1)
1782        polygon1.SetLines(lines1)
1783
1784        lines2 = vtki.vtkCellArray()
1785        lines2.InsertNextCell(poly2.GetNumberOfPoints())
1786        for i in range(poly2.GetNumberOfPoints()):
1787            lines2.InsertCellPoint(i)
1788
1789        polygon2 = vtki.vtkPolyData()
1790        polygon2.SetPoints(ppoints2)
1791        polygon2.SetLines(lines2)
1792
1793        merged_pd = vtki.new("AppendPolyData")
1794        merged_pd.AddInputData(polygon1)
1795        merged_pd.AddInputData(polygon2)
1796        merged_pd.Update()
1797
1798        rsf = vtki.new("RuledSurfaceFilter")
1799        rsf.CloseSurfaceOff()
1800        rsf.SetRuledMode(mode)
1801        rsf.SetResolution(res[0], res[1])
1802        rsf.SetInputData(merged_pd.GetOutput())
1803        rsf.Update()
1804        # convert triangle strips to polygons
1805        tris = vtki.new("TriangleFilter")
1806        tris.SetInputData(rsf.GetOutput())
1807        tris.Update()
1808        out = tris.GetOutput()
1809
1810        super().__init__(out, c, alpha)
1811
1812        self.name = "Ribbon"
1813
1814
1815class Arrow(Mesh):
1816    """
1817    Build a 3D arrow from `start_pt` to `end_pt` of section size `s`,
1818    expressed as the fraction of the window size.
1819    """
1820
1821    def __init__(
1822        self,
1823        start_pt=(0, 0, 0),
1824        end_pt=(1, 0, 0),
1825        s=None,
1826        shaft_radius=None,
1827        head_radius=None,
1828        head_length=None,
1829        res=12,
1830        c="r4",
1831        alpha=1.0,
1832    ) -> None:
1833        """
1834        If `c` is a `float` less than 1, the arrow is rendered as a in a color scale
1835        from white to red.
1836
1837        .. note:: If `s=None` the arrow is scaled proportionally to its length
1838
1839        ![](https://raw.githubusercontent.com/lorensen/VTKExamples/master/src/Testing/Baseline/Cxx/GeometricObjects/TestOrientedArrow.png)
1840        """
1841        # in case user is passing meshs
1842        if isinstance(start_pt, vtki.vtkActor):
1843            start_pt = start_pt.GetPosition()
1844        if isinstance(end_pt, vtki.vtkActor):
1845            end_pt = end_pt.GetPosition()
1846
1847        axis = np.asarray(end_pt) - np.asarray(start_pt)
1848        length = float(np.linalg.norm(axis))
1849        if length:
1850            axis = axis / length
1851        if len(axis) < 3:  # its 2d
1852            theta = np.pi / 2
1853            start_pt = [start_pt[0], start_pt[1], 0.0]
1854            end_pt = [end_pt[0], end_pt[1], 0.0]
1855        else:
1856            theta = np.arccos(axis[2])
1857        phi = np.arctan2(axis[1], axis[0])
1858        self.source = vtki.new("ArrowSource")
1859        self.source.SetShaftResolution(res)
1860        self.source.SetTipResolution(res)
1861
1862        if s:
1863            sz = 0.02
1864            self.source.SetTipRadius(sz)
1865            self.source.SetShaftRadius(sz / 1.75)
1866            self.source.SetTipLength(sz * 15)
1867
1868        if head_length:
1869            self.source.SetTipLength(head_length)
1870        if head_radius:
1871            self.source.SetTipRadius(head_radius)
1872        if shaft_radius:
1873            self.source.SetShaftRadius(shaft_radius)
1874
1875        self.source.Update()
1876
1877        t = vtki.vtkTransform()
1878        t.Translate(start_pt)
1879        t.RotateZ(np.rad2deg(phi))
1880        t.RotateY(np.rad2deg(theta))
1881        t.RotateY(-90)  # put it along Z
1882        if s:
1883            sz = 800 * s
1884            t.Scale(length, sz, sz)
1885        else:
1886            t.Scale(length, length, length)
1887
1888        tf = vtki.new("TransformPolyDataFilter")
1889        tf.SetInputData(self.source.GetOutput())
1890        tf.SetTransform(t)
1891        tf.Update()
1892
1893        super().__init__(tf.GetOutput(), c, alpha)
1894
1895        self.transform = LinearTransform().translate(start_pt)
1896        # self.pos(start_pt)
1897
1898        self.phong().lighting("plastic")
1899        self.actor.PickableOff()
1900        self.actor.DragableOff()
1901        self.base = np.array(start_pt, dtype=float)  # used by pyplot
1902        self.top  = np.array(end_pt,   dtype=float)  # used by pyplot
1903        self.top_index = None
1904        self.fill = True                    # used by pyplot.__iadd__()
1905        self.s = s if s is not None else 1  # used by pyplot.__iadd__()
1906        self.name = "Arrow"
1907
1908
1909class Arrows(Glyph):
1910    """
1911    Build arrows between two lists of points.
1912    """
1913
1914    def __init__(
1915        self,
1916        start_pts,
1917        end_pts=None,
1918        s=None,
1919        shaft_radius=None,
1920        head_radius=None,
1921        head_length=None,
1922        thickness=1.0,
1923        res=6,
1924        c='k3',
1925        alpha=1.0,
1926    ) -> None:
1927        """
1928        Build arrows between two lists of points `start_pts` and `end_pts`.
1929         `start_pts` can be also passed in the form `[[point1, point2], ...]`.
1930
1931        Color can be specified as a colormap which maps the size of the arrows.
1932
1933        Arguments:
1934            s : (float)
1935                fix aspect-ratio of the arrow and scale its cross section
1936            c : (color)
1937                color or color map name
1938            alpha : (float)
1939                set object opacity
1940            res : (int)
1941                set arrow resolution
1942
1943        Examples:
1944            - [glyphs2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/glyphs2.py)
1945
1946            ![](https://user-images.githubusercontent.com/32848391/55897850-a1a0da80-5bc1-11e9-81e0-004c8f396b43.jpg)
1947        """
1948        if isinstance(start_pts, Points):
1949            start_pts = start_pts.vertices
1950        if isinstance(end_pts, Points):
1951            end_pts = end_pts.vertices
1952
1953        start_pts = np.asarray(start_pts)
1954        if end_pts is None:
1955            strt = start_pts[:, 0]
1956            end_pts = start_pts[:, 1]
1957            start_pts = strt
1958        else:
1959            end_pts = np.asarray(end_pts)
1960
1961        start_pts = utils.make3d(start_pts)
1962        end_pts = utils.make3d(end_pts)
1963
1964        arr = vtki.new("ArrowSource")
1965        arr.SetShaftResolution(res)
1966        arr.SetTipResolution(res)
1967
1968        if s:
1969            sz = 0.02 * s
1970            arr.SetTipRadius(sz * 2)
1971            arr.SetShaftRadius(sz * thickness)
1972            arr.SetTipLength(sz * 10)
1973
1974        if head_radius:
1975            arr.SetTipRadius(head_radius)
1976        if shaft_radius:
1977            arr.SetShaftRadius(shaft_radius)
1978        if head_length:
1979            arr.SetTipLength(head_length)
1980
1981        arr.Update()
1982        out = arr.GetOutput()
1983
1984        orients = end_pts - start_pts
1985
1986        color_by_vector_size = utils.is_sequence(c) or c in cmaps_names
1987
1988        super().__init__(
1989            start_pts,
1990            out,
1991            orientation_array=orients,
1992            scale_by_vector_size=True,
1993            color_by_vector_size=color_by_vector_size,
1994            c=c,
1995            alpha=alpha,
1996        )
1997        self.lighting("off")
1998        if color_by_vector_size:
1999            vals = np.linalg.norm(orients, axis=1)
2000            self.mapper.SetScalarRange(vals.min(), vals.max())
2001        else:
2002            self.c(c)
2003        self.name = "Arrows"
2004
2005
2006class Arrow2D(Mesh):
2007    """
2008    Build a 2D arrow.
2009    """
2010
2011    def __init__(
2012        self,
2013        start_pt=(0, 0, 0),
2014        end_pt=(1, 0, 0),
2015        s=1,
2016        rotation=0.0,
2017        shaft_length=0.85,
2018        shaft_width=0.055,
2019        head_length=0.175,
2020        head_width=0.175,
2021        fill=True,
2022        c="red4",
2023        alpha=1.0,
2024   ) -> None:
2025        """
2026        Build a 2D arrow from `start_pt` to `end_pt`.
2027
2028        Arguments:
2029            s : (float)
2030                a global multiplicative convenience factor controlling the arrow size
2031            shaft_length : (float)
2032                fractional shaft length
2033            shaft_width : (float)
2034                fractional shaft width
2035            head_length : (float)
2036                fractional head length
2037            head_width : (float)
2038                fractional head width
2039            fill : (bool)
2040                if False only generate the outline
2041        """
2042        self.fill = fill  ## needed by pyplot.__iadd()
2043        self.s = s        ## needed by pyplot.__iadd()
2044
2045        if s != 1:
2046            shaft_width *= s
2047            head_width *= np.sqrt(s)
2048
2049        # in case user is passing meshs
2050        if isinstance(start_pt, vtki.vtkActor):
2051            start_pt = start_pt.GetPosition()
2052        if isinstance(end_pt, vtki.vtkActor):
2053            end_pt = end_pt.GetPosition()
2054        if len(start_pt) == 2:
2055            start_pt = [start_pt[0], start_pt[1], 0]
2056        if len(end_pt) == 2:
2057            end_pt = [end_pt[0], end_pt[1], 0]
2058
2059        headBase = 1 - head_length
2060        head_width = max(head_width, shaft_width)
2061        if head_length is None or headBase > shaft_length:
2062            headBase = shaft_length
2063
2064        verts = []
2065        verts.append([0, -shaft_width / 2, 0])
2066        verts.append([shaft_length, -shaft_width / 2, 0])
2067        verts.append([headBase, -head_width / 2, 0])
2068        verts.append([1, 0, 0])
2069        verts.append([headBase, head_width / 2, 0])
2070        verts.append([shaft_length, shaft_width / 2, 0])
2071        verts.append([0, shaft_width / 2, 0])
2072        if fill:
2073            faces = ((0, 1, 3, 5, 6), (5, 3, 4), (1, 2, 3))
2074            poly = utils.buildPolyData(verts, faces)
2075        else:
2076            lines = (0, 1, 2, 3, 4, 5, 6, 0)
2077            poly = utils.buildPolyData(verts, [], lines=lines)
2078
2079        axis = np.array(end_pt) - np.array(start_pt)
2080        length = float(np.linalg.norm(axis))
2081        if length:
2082            axis = axis / length
2083        theta = 0
2084        if len(axis) > 2:
2085            theta = np.arccos(axis[2])
2086        phi = np.arctan2(axis[1], axis[0])
2087
2088        t = vtki.vtkTransform()
2089        t.Translate(start_pt)
2090        if phi:
2091            t.RotateZ(np.rad2deg(phi))
2092        if theta:
2093            t.RotateY(np.rad2deg(theta))
2094        t.RotateY(-90)  # put it along Z
2095        if rotation:
2096            t.RotateX(rotation)
2097        t.Scale(length, length, length)
2098
2099        tf = vtki.new("TransformPolyDataFilter")
2100        tf.SetInputData(poly)
2101        tf.SetTransform(t)
2102        tf.Update()
2103
2104        super().__init__(tf.GetOutput(), c, alpha)
2105
2106        self.transform = LinearTransform().translate(start_pt)
2107
2108        self.lighting("off")
2109        self.actor.DragableOff()
2110        self.actor.PickableOff()
2111        self.base = np.array(start_pt, dtype=float) # used by pyplot
2112        self.top  = np.array(end_pt,   dtype=float) # used by pyplot
2113        self.name = "Arrow2D"
2114
2115
2116class Arrows2D(Glyph):
2117    """
2118    Build 2D arrows between two lists of points.
2119    """
2120
2121    def __init__(
2122        self,
2123        start_pts,
2124        end_pts=None,
2125        s=1.0,
2126        rotation=0.0,
2127        shaft_length=0.8,
2128        shaft_width=0.05,
2129        head_length=0.225,
2130        head_width=0.175,
2131        fill=True,
2132        c=None,
2133        alpha=1.0,
2134    ) -> None:
2135        """
2136        Build 2D arrows between two lists of points `start_pts` and `end_pts`.
2137        `start_pts` can be also passed in the form `[[point1, point2], ...]`.
2138
2139        Color can be specified as a colormap which maps the size of the arrows.
2140
2141        Arguments:
2142            shaft_length : (float)
2143                fractional shaft length
2144            shaft_width : (float)
2145                fractional shaft width
2146            head_length : (float)
2147                fractional head length
2148            head_width : (float)
2149                fractional head width
2150            fill : (bool)
2151                if False only generate the outline
2152        """
2153        if isinstance(start_pts, Points):
2154            start_pts = start_pts.vertices
2155        if isinstance(end_pts, Points):
2156            end_pts = end_pts.vertices
2157
2158        start_pts = np.asarray(start_pts, dtype=float)
2159        if end_pts is None:
2160            strt = start_pts[:, 0]
2161            end_pts = start_pts[:, 1]
2162            start_pts = strt
2163        else:
2164            end_pts = np.asarray(end_pts, dtype=float)
2165
2166        if head_length is None:
2167            head_length = 1 - shaft_length
2168
2169        arr = Arrow2D(
2170            (0, 0, 0),
2171            (1, 0, 0),
2172            s=s,
2173            rotation=rotation,
2174            shaft_length=shaft_length,
2175            shaft_width=shaft_width,
2176            head_length=head_length,
2177            head_width=head_width,
2178            fill=fill,
2179        )
2180
2181        orients = end_pts - start_pts
2182        orients = utils.make3d(orients)
2183
2184        pts = Points(start_pts)
2185        super().__init__(
2186            pts,
2187            arr,
2188            orientation_array=orients,
2189            scale_by_vector_size=True,
2190            c=c,
2191            alpha=alpha,
2192        )
2193        self.flat().lighting("off").pickable(False)
2194        if c is not None:
2195            self.color(c)
2196        self.name = "Arrows2D"
2197
2198
2199class FlatArrow(Ribbon):
2200    """
2201    Build a 2D arrow in 3D space by joining two close lines.
2202    """
2203
2204    def __init__(self, line1, line2, tip_size=1.0, tip_width=1.0) -> None:
2205        """
2206        Build a 2D arrow in 3D space by joining two close lines.
2207
2208        Examples:
2209            - [flatarrow.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/flatarrow.py)
2210
2211                ![](https://vedo.embl.es/images/basic/flatarrow.png)
2212        """
2213        if isinstance(line1, Points):
2214            line1 = line1.vertices
2215        if isinstance(line2, Points):
2216            line2 = line2.vertices
2217
2218        sm1, sm2 = np.array(line1[-1], dtype=float), np.array(line2[-1], dtype=float)
2219
2220        v = (sm1 - sm2) / 3 * tip_width
2221        p1 = sm1 + v
2222        p2 = sm2 - v
2223        pm1 = (sm1 + sm2) / 2
2224        pm2 = (np.array(line1[-2]) + np.array(line2[-2])) / 2
2225        pm12 = pm1 - pm2
2226        tip = pm12 / np.linalg.norm(pm12) * np.linalg.norm(v) * 3 * tip_size / tip_width + pm1
2227
2228        line1.append(p1)
2229        line1.append(tip)
2230        line2.append(p2)
2231        line2.append(tip)
2232        resm = max(100, len(line1))
2233
2234        super().__init__(line1, line2, res=(resm, 1))
2235        self.phong().lighting("off")
2236        self.actor.PickableOff()
2237        self.actor.DragableOff()
2238        self.name = "FlatArrow"
2239
2240
2241class Triangle(Mesh):
2242    """Create a triangle from 3 points in space."""
2243
2244    def __init__(self, p1, p2, p3, c="green7", alpha=1.0) -> None:
2245        """Create a triangle from 3 points in space."""
2246        super().__init__([[p1, p2, p3], [[0, 1, 2]]], c, alpha)
2247        self.properties.LightingOff()
2248        self.name = "Triangle"
2249
2250
2251class Polygon(Mesh):
2252    """
2253    Build a polygon in the `xy` plane.
2254    """
2255
2256    def __init__(self, pos=(0, 0, 0), nsides=6, r=1.0, c="coral", alpha=1.0) -> None:
2257        """
2258        Build a polygon in the `xy` plane of `nsides` of radius `r`.
2259
2260        ![](https://raw.githubusercontent.com/lorensen/VTKExamples/master/src/Testing/Baseline/Cxx/GeometricObjects/TestRegularPolygonSource.png)
2261        """
2262        t = np.linspace(np.pi / 2, 5 / 2 * np.pi, num=nsides, endpoint=False)
2263        pts = pol2cart(np.ones_like(t) * r, t).T
2264        faces = [list(range(nsides))]
2265        # do not use: vtkRegularPolygonSource
2266        super().__init__([pts, faces], c, alpha)
2267        if len(pos) == 2:
2268            pos = (pos[0], pos[1], 0)
2269        self.pos(pos)
2270        self.properties.LightingOff()
2271        self.name = "Polygon " + str(nsides)
2272
2273
2274class Circle(Polygon):
2275    """
2276    Build a Circle of radius `r`.
2277    """
2278
2279    def __init__(self, pos=(0, 0, 0), r=1.0, res=120, c="gray5", alpha=1.0) -> None:
2280        """
2281        Build a Circle of radius `r`.
2282        """
2283        super().__init__(pos, nsides=res, r=r)
2284
2285        self.nr_of_points = 0
2286        self.va = 0
2287        self.vb = 0
2288        self.axis1: List[float] = []
2289        self.axis2: List[float] = []
2290        self.center: List[float] = []  # filled by pointcloud.pca_ellipse()
2291        self.pvalue = 0.0              # filled by pointcloud.pca_ellipse()
2292        self.alpha(alpha).c(c)
2293        self.name = "Circle"
2294    
2295    def acircularity(self) -> float:
2296        """
2297        Return a measure of how different an ellipse is from a circle.
2298        Values close to zero correspond to a circular object.
2299        """
2300        a, b = self.va, self.vb
2301        value = 0.0
2302        if a+b:
2303            value = ((a-b)/(a+b))**2
2304        return value
2305
2306class GeoCircle(Polygon):
2307    """
2308    Build a Circle of radius `r`.
2309    """
2310
2311    def __init__(self, lat, lon, r=1.0, res=60, c="red4", alpha=1.0) -> None:
2312        """
2313        Build a Circle of radius `r` as projected on a geographic map.
2314        Circles near the poles will look very squashed.
2315
2316        See example:
2317            ```bash
2318            vedo -r earthquake
2319            ```
2320        """
2321        coords = []
2322        sinr, cosr = np.sin(r), np.cos(r)
2323        sinlat, coslat = np.sin(lat), np.cos(lat)
2324        for phi in np.linspace(0, 2 * np.pi, num=res, endpoint=False):
2325            clat = np.arcsin(sinlat * cosr + coslat * sinr * np.cos(phi))
2326            clng = lon + np.arctan2(np.sin(phi) * sinr * coslat, cosr - sinlat * np.sin(clat))
2327            coords.append([clng / np.pi + 1, clat * 2 / np.pi + 1, 0])
2328
2329        super().__init__(nsides=res, c=c, alpha=alpha)
2330        self.vertices = coords # warp polygon points to match geo projection
2331        self.name = "Circle"
2332
2333
2334class Star(Mesh):
2335    """
2336    Build a 2D star shape.
2337    """
2338
2339    def __init__(self, pos=(0, 0, 0), n=5, r1=0.7, r2=1.0, line=False, c="blue6", alpha=1.0) -> None:
2340        """
2341        Build a 2D star shape of `n` cusps of inner radius `r1` and outer radius `r2`.
2342
2343        If line is True then only build the outer line (no internal surface meshing).
2344
2345        Example:
2346            - [extrude.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/extrude.py)
2347
2348                ![](https://vedo.embl.es/images/basic/extrude.png)
2349        """
2350        t = np.linspace(np.pi / 2, 5 / 2 * np.pi, num=n, endpoint=False)
2351        x, y = pol2cart(np.ones_like(t) * r2, t)
2352        pts = np.c_[x, y, np.zeros_like(x)]
2353
2354        apts = []
2355        for i, p in enumerate(pts):
2356            apts.append(p)
2357            if i + 1 < n:
2358                apts.append((p + pts[i + 1]) / 2 * r1 / r2)
2359        apts.append((pts[-1] + pts[0]) / 2 * r1 / r2)
2360
2361        if line:
2362            apts.append(pts[0])
2363            poly = utils.buildPolyData(apts, lines=list(range(len(apts))))
2364            super().__init__(poly, c, alpha)
2365            self.lw(2)
2366        else:
2367            apts.append((0, 0, 0))
2368            cells = []
2369            for i in range(2 * n - 1):
2370                cell = [2 * n, i, i + 1]
2371                cells.append(cell)
2372            cells.append([2 * n, i + 1, 0])
2373            super().__init__([apts, cells], c, alpha)
2374
2375        if len(pos) == 2:
2376            pos = (pos[0], pos[1], 0)
2377
2378        self.properties.LightingOff()
2379        self.name = "Star"
2380
2381
2382class Disc(Mesh):
2383    """
2384    Build a 2D disc.
2385    """
2386
2387    def __init__(
2388        self, pos=(0, 0, 0), r1=0.5, r2=1.0, res=(1, 120), angle_range=(), c="gray4", alpha=1.0
2389    ) -> None:
2390        """
2391        Build a 2D disc of inner radius `r1` and outer radius `r2`.
2392
2393        Set `res` as the resolution in R and Phi (can be a list).
2394
2395        Use `angle_range` to create a disc sector between the 2 specified angles.
2396
2397        ![](https://raw.githubusercontent.com/lorensen/VTKExamples/master/src/Testing/Baseline/Cxx/GeometricObjects/TestDisk.png)
2398        """
2399        if utils.is_sequence(res):
2400            res_r, res_phi = res
2401        else:
2402            res_r, res_phi = res, 12 * res
2403
2404        if len(angle_range) == 0:
2405            ps = vtki.new("DiskSource")
2406        else:
2407            ps = vtki.new("SectorSource")
2408            ps.SetStartAngle(angle_range[0])
2409            ps.SetEndAngle(angle_range[1])
2410
2411        ps.SetInnerRadius(r1)
2412        ps.SetOuterRadius(r2)
2413        ps.SetRadialResolution(res_r)
2414        ps.SetCircumferentialResolution(res_phi)
2415        ps.Update()
2416        super().__init__(ps.GetOutput(), c, alpha)
2417        self.flat()
2418        self.pos(utils.make3d(pos))
2419        self.name = "Disc"
2420
2421
2422class Arc(Mesh):
2423    """
2424    Build a 2D circular arc between 2 points.
2425    """
2426
2427    def __init__(
2428        self,
2429        center,
2430        point1,
2431        point2=None,
2432        normal=None,
2433        angle=None,
2434        invert=False,
2435        res=50,
2436        c="gray4",
2437        alpha=1.0,
2438    ) -> None:
2439        """
2440        Build a 2D circular arc between 2 points `point1` and `point2`.
2441
2442        If `normal` is specified then `center` is ignored, and
2443        normal vector, a starting `point1` (polar vector)
2444        and an angle defining the arc length need to be assigned.
2445
2446        Arc spans the shortest angular sector point1 and point2,
2447        if `invert=True`, then the opposite happens.
2448        """
2449        if len(point1) == 2:
2450            point1 = (point1[0], point1[1], 0)
2451        if point2 is not None and len(point2) == 2:
2452            point2 = (point2[0], point2[1], 0)
2453
2454        ar = vtki.new("ArcSource")
2455        if point2 is not None:
2456            self.top = point2
2457            point2 = point2 - np.asarray(point1)
2458            ar.UseNormalAndAngleOff()
2459            ar.SetPoint1([0, 0, 0])
2460            ar.SetPoint2(point2)
2461            # ar.SetCenter(center)
2462        elif normal is not None and angle is not None:
2463            ar.UseNormalAndAngleOn()
2464            ar.SetAngle(angle)
2465            ar.SetPolarVector(point1)
2466            ar.SetNormal(normal)
2467        else:
2468            vedo.logger.error("incorrect input combination")
2469            return
2470        ar.SetNegative(invert)
2471        ar.SetResolution(res)
2472        ar.Update()
2473
2474        super().__init__(ar.GetOutput(), c, alpha)
2475        self.pos(center)
2476        self.lw(2).lighting("off")
2477        self.name = "Arc"
2478
2479
2480class IcoSphere(Mesh):
2481    """
2482    Create a sphere made of a uniform triangle mesh.
2483    """
2484
2485    def __init__(self, pos=(0, 0, 0), r=1.0, subdivisions=4, c="r5", alpha=1.0) -> None:
2486        """
2487        Create a sphere made of a uniform triangle mesh
2488        (from recursive subdivision of an icosahedron).
2489
2490        Example:
2491        ```python
2492        from vedo import *
2493        icos = IcoSphere(subdivisions=3)
2494        icos.compute_quality().cmap('coolwarm')
2495        icos.show(axes=1).close()
2496        ```
2497        ![](https://vedo.embl.es/images/basic/icosphere.jpg)
2498        """
2499        subdivisions = int(min(subdivisions, 9))  # to avoid disasters
2500
2501        t = (1.0 + np.sqrt(5.0)) / 2.0
2502        points = np.array(
2503            [
2504                [-1, t, 0],
2505                [1, t, 0],
2506                [-1, -t, 0],
2507                [1, -t, 0],
2508                [0, -1, t],
2509                [0, 1, t],
2510                [0, -1, -t],
2511                [0, 1, -t],
2512                [t, 0, -1],
2513                [t, 0, 1],
2514                [-t, 0, -1],
2515                [-t, 0, 1],
2516            ]
2517        )
2518        faces = [
2519            [0, 11, 5],
2520            [0, 5, 1],
2521            [0, 1, 7],
2522            [0, 7, 10],
2523            [0, 10, 11],
2524            [1, 5, 9],
2525            [5, 11, 4],
2526            [11, 10, 2],
2527            [10, 7, 6],
2528            [7, 1, 8],
2529            [3, 9, 4],
2530            [3, 4, 2],
2531            [3, 2, 6],
2532            [3, 6, 8],
2533            [3, 8, 9],
2534            [4, 9, 5],
2535            [2, 4, 11],
2536            [6, 2, 10],
2537            [8, 6, 7],
2538            [9, 8, 1],
2539        ]
2540        super().__init__([points * r, faces], c=c, alpha=alpha)
2541
2542        for _ in range(subdivisions):
2543            self.subdivide(method=1)
2544            pts = utils.versor(self.vertices) * r
2545            self.vertices = pts
2546
2547        self.pos(pos)
2548        self.name = "IcoSphere"
2549
2550
2551class Sphere(Mesh):
2552    """
2553    Build a sphere.
2554    """
2555
2556    def __init__(self, pos=(0, 0, 0), r=1.0, res=24, quads=False, c="r5", alpha=1.0) -> None:
2557        """
2558        Build a sphere at position `pos` of radius `r`.
2559
2560        Arguments:
2561            r : (float)
2562                sphere radius
2563            res : (int, list)
2564                resolution in phi, resolution in theta is by default `2*res`
2565            quads : (bool)
2566                sphere mesh will be made of quads instead of triangles
2567
2568        [](https://user-images.githubusercontent.com/32848391/72433092-f0a31e00-3798-11ea-85f7-b2f5fcc31568.png)
2569        """
2570        if len(pos) == 2:
2571            pos = np.asarray([pos[0], pos[1], 0])
2572
2573        self.radius = r  # used by fitSphere
2574        self.center = pos
2575        self.residue = 0
2576
2577        if quads:
2578            res = max(res, 4)
2579            img = vtki.vtkImageData()
2580            img.SetDimensions(res - 1, res - 1, res - 1)
2581            rs = 1.0 / (res - 2)
2582            img.SetSpacing(rs, rs, rs)
2583            gf = vtki.new("GeometryFilter")
2584            gf.SetInputData(img)
2585            gf.Update()
2586            super().__init__(gf.GetOutput(), c, alpha)
2587            self.lw(0.1)
2588
2589            cgpts = self.vertices - (0.5, 0.5, 0.5)
2590
2591            x, y, z = cgpts.T
2592            x = x * (1 + x * x) / 2
2593            y = y * (1 + y * y) / 2
2594            z = z * (1 + z * z) / 2
2595            _, theta, phi = cart2spher(x, y, z)
2596
2597            pts = spher2cart(np.ones_like(phi) * r, theta, phi).T
2598            self.vertices = pts
2599
2600        else:
2601            if utils.is_sequence(res):
2602                res_t, res_phi = res
2603            else:
2604                res_t, res_phi = 2 * res, res
2605
2606            ss = vtki.new("SphereSource")
2607            ss.SetRadius(r)
2608            ss.SetThetaResolution(res_t)
2609            ss.SetPhiResolution(res_phi)
2610            ss.Update()
2611
2612            super().__init__(ss.GetOutput(), c, alpha)
2613
2614        self.phong()
2615        self.pos(pos)
2616        self.name = "Sphere"
2617
2618
2619class Spheres(Mesh):
2620    """
2621    Build a large set of spheres.
2622    """
2623
2624    def __init__(self, centers, r=1.0, res=8, c="red5", alpha=1) -> None:
2625        """
2626        Build a (possibly large) set of spheres at `centers` of radius `r`.
2627
2628        Either `c` or `r` can be a list of RGB colors or radii.
2629
2630        Examples:
2631            - [manyspheres.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/manyspheres.py)
2632
2633            ![](https://vedo.embl.es/images/basic/manyspheres.jpg)
2634        """
2635
2636        if isinstance(centers, Points):
2637            centers = centers.vertices
2638        centers = np.asarray(centers, dtype=float)
2639        base = centers[0]
2640
2641        cisseq = False
2642        if utils.is_sequence(c):
2643            cisseq = True
2644
2645        if cisseq:
2646            if len(centers) != len(c):
2647                vedo.logger.error(f"mismatch #centers {len(centers)} != {len(c)} #colors")
2648                raise RuntimeError()
2649
2650        risseq = False
2651        if utils.is_sequence(r):
2652            risseq = True
2653
2654        if risseq:
2655            if len(centers) != len(r):
2656                vedo.logger.error(f"mismatch #centers {len(centers)} != {len(r)} #radii")
2657                raise RuntimeError()
2658        if cisseq and risseq:
2659            vedo.logger.error("Limitation: c and r cannot be both sequences.")
2660            raise RuntimeError()
2661
2662        src = vtki.new("SphereSource")
2663        if not risseq:
2664            src.SetRadius(r)
2665        if utils.is_sequence(res):
2666            res_t, res_phi = res
2667        else:
2668            res_t, res_phi = 2 * res, res
2669
2670        src.SetThetaResolution(res_t)
2671        src.SetPhiResolution(res_phi)
2672        src.Update()
2673
2674        psrc = vtki.new("PointSource")
2675        psrc.SetNumberOfPoints(len(centers))
2676        psrc.Update()
2677        pd = psrc.GetOutput()
2678        vpts = pd.GetPoints()
2679
2680        glyph = vtki.vtkGlyph3D()
2681        glyph.SetSourceConnection(src.GetOutputPort())
2682
2683        if cisseq:
2684            glyph.SetColorModeToColorByScalar()
2685            ucols = vtki.vtkUnsignedCharArray()
2686            ucols.SetNumberOfComponents(3)
2687            ucols.SetName("Colors")
2688            for acol in c:
2689                cx, cy, cz = get_color(acol)
2690                ucols.InsertNextTuple3(cx * 255, cy * 255, cz * 255)
2691            pd.GetPointData().AddArray(ucols)
2692            pd.GetPointData().SetActiveScalars("Colors")
2693            glyph.ScalingOff()
2694        elif risseq:
2695            glyph.SetScaleModeToScaleByScalar()
2696            urads = utils.numpy2vtk(2 * np.ascontiguousarray(r), dtype=np.float32)
2697            urads.SetName("Radii")
2698            pd.GetPointData().AddArray(urads)
2699            pd.GetPointData().SetActiveScalars("Radii")
2700
2701        vpts.SetData(utils.numpy2vtk(centers - base, dtype=np.float32))
2702
2703        glyph.SetInputData(pd)
2704        glyph.Update()
2705
2706        super().__init__(glyph.GetOutput(), alpha=alpha)
2707        self.pos(base)
2708        self.phong()
2709        if cisseq:
2710            self.mapper.ScalarVisibilityOn()
2711        else:
2712            self.mapper.ScalarVisibilityOff()
2713            self.c(c)
2714        self.name = "Spheres"
2715
2716
2717class Earth(Mesh):
2718    """
2719    Build a textured mesh representing the Earth.
2720    """
2721
2722    def __init__(self, style=1, r=1.0) -> None:
2723        """
2724        Build a textured mesh representing the Earth.
2725
2726        Example:
2727            - [geodesic_curve.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/geodesic_curve.py)
2728
2729                ![](https://vedo.embl.es/images/advanced/geodesic.png)
2730        """
2731        tss = vtki.new("TexturedSphereSource")
2732        tss.SetRadius(r)
2733        tss.SetThetaResolution(72)
2734        tss.SetPhiResolution(36)
2735        tss.Update()
2736        super().__init__(tss.GetOutput(), c="w")
2737        atext = vtki.vtkTexture()
2738        pnm_reader = vtki.new("JPEGReader")
2739        fn = vedo.file_io.download(vedo.dataurl + f"textures/earth{style}.jpg", verbose=False)
2740        pnm_reader.SetFileName(fn)
2741        atext.SetInputConnection(pnm_reader.GetOutputPort())
2742        atext.InterpolateOn()
2743        self.texture(atext)
2744        self.name = "Earth"
2745
2746
2747class Ellipsoid(Mesh):
2748    """Build a 3D ellipsoid."""
2749    def __init__(
2750        self,
2751        pos=(0, 0, 0),
2752        axis1=(0.5, 0, 0),
2753        axis2=(0, 1, 0),
2754        axis3=(0, 0, 1.5),
2755        res=24,
2756        c="cyan4",
2757        alpha=1.0,
2758    ) -> None:
2759        """
2760        Build a 3D ellipsoid centered at position `pos`.
2761
2762        Arguments:
2763            axis1 : (list)
2764                First axis. Length corresponds to semi-axis.
2765            axis2 : (list)
2766                Second axis. Length corresponds to semi-axis.
2767            axis3 : (list)
2768                Third axis. Length corresponds to semi-axis.
2769        """        
2770        self.center = utils.make3d(pos)
2771
2772        self.axis1 = utils.make3d(axis1)
2773        self.axis2 = utils.make3d(axis2)
2774        self.axis3 = utils.make3d(axis3)
2775
2776        self.va = np.linalg.norm(self.axis1)
2777        self.vb = np.linalg.norm(self.axis2)
2778        self.vc = np.linalg.norm(self.axis3)
2779
2780        self.va_error = 0
2781        self.vb_error = 0
2782        self.vc_error = 0
2783
2784        self.nr_of_points = 1  # used by pointcloud.pca_ellipsoid()
2785        self.pvalue = 0        # used by pointcloud.pca_ellipsoid()
2786
2787        if utils.is_sequence(res):
2788            res_t, res_phi = res
2789        else:
2790            res_t, res_phi = 2 * res, res
2791
2792        elli_source = vtki.new("SphereSource")
2793        elli_source.SetRadius(1)
2794        elli_source.SetThetaResolution(res_t)
2795        elli_source.SetPhiResolution(res_phi)
2796        elli_source.Update()
2797
2798        super().__init__(elli_source.GetOutput(), c, alpha)
2799
2800        matrix = np.c_[self.axis1, self.axis2, self.axis3]
2801        lt = LinearTransform(matrix).translate(pos)
2802        self.apply_transform(lt)
2803        self.name = "Ellipsoid"
2804
2805    def asphericity(self) -> float:
2806        """
2807        Return a measure of how different an ellipsoid is from a sphere.
2808        Values close to zero correspond to a spheric object.
2809        """
2810        a, b, c = self.va, self.vb, self.vc
2811        asp = ( ((a-b)/(a+b))**2
2812              + ((a-c)/(a+c))**2
2813              + ((b-c)/(b+c))**2 ) / 3. * 4.
2814        return float(asp)
2815
2816    def asphericity_error(self) -> float:
2817        """
2818        Calculate statistical error on the asphericity value.
2819
2820        Errors on the main axes are stored in
2821        `Ellipsoid.va_error`, Ellipsoid.vb_error` and `Ellipsoid.vc_error`.
2822        """
2823        a, b, c = self.va, self.vb, self.vc
2824        sqrtn = np.sqrt(self.nr_of_points)
2825        ea, eb, ec = a / 2 / sqrtn, b / 2 / sqrtn, b / 2 / sqrtn
2826
2827        # from sympy import *
2828        # init_printing(use_unicode=True)
2829        # a, b, c, ea, eb, ec = symbols("a b c, ea, eb,ec")
2830        # L = (
2831        #    (((a - b) / (a + b)) ** 2 + ((c - b) / (c + b)) ** 2 + ((a - c) / (a + c)) ** 2)
2832        #    / 3 * 4)
2833        # dl2 = (diff(L, a) * ea) ** 2 + (diff(L, b) * eb) ** 2 + (diff(L, c) * ec) ** 2
2834        # print(dl2)
2835        # exit()
2836
2837        dL2 = (
2838            ea ** 2
2839            * (
2840                -8 * (a - b) ** 2 / (3 * (a + b) ** 3)
2841                - 8 * (a - c) ** 2 / (3 * (a + c) ** 3)
2842                + 4 * (2 * a - 2 * c) / (3 * (a + c) ** 2)
2843                + 4 * (2 * a - 2 * b) / (3 * (a + b) ** 2)
2844            ) ** 2
2845            + eb ** 2
2846            * (
2847                4 * (-2 * a + 2 * b) / (3 * (a + b) ** 2)
2848                - 8 * (a - b) ** 2 / (3 * (a + b) ** 3)
2849                - 8 * (-b + c) ** 2 / (3 * (b + c) ** 3)
2850                + 4 * (2 * b - 2 * c) / (3 * (b + c) ** 2)
2851            ) ** 2
2852            + ec ** 2
2853            * (
2854                4 * (-2 * a + 2 * c) / (3 * (a + c) ** 2)
2855                - 8 * (a - c) ** 2 / (3 * (a + c) ** 3)
2856                + 4 * (-2 * b + 2 * c) / (3 * (b + c) ** 2)
2857                - 8 * (-b + c) ** 2 / (3 * (b + c) ** 3)
2858            ) ** 2
2859        )
2860        err = np.sqrt(dL2)
2861        self.va_error = ea
2862        self.vb_error = eb
2863        self.vc_error = ec
2864        return err
2865
2866
2867class Grid(Mesh):
2868    """
2869    An even or uneven 2D grid.
2870    """
2871
2872    def __init__(self, pos=(0, 0, 0), s=(1, 1), res=(10, 10), lw=1, c="k3", alpha=1.0) -> None:
2873        """
2874        Create an even or uneven 2D grid.
2875        Can also be created from a `np.mgrid` object (see example).
2876
2877        Arguments:
2878            pos : (list, Points, Mesh)
2879                position in space, can also be passed as a bounding box [xmin,xmax, ymin,ymax].
2880            s : (float, list)
2881                if a float is provided it is interpreted as the total size along x and y,
2882                if a list of coords is provided they are interpreted as the vertices of the grid along x and y.
2883                In this case keyword `res` is ignored (see example below).
2884            res : (list)
2885                resolutions along x and y, e.i. the number of subdivisions
2886            lw : (int)
2887                line width
2888
2889        Example:
2890            ```python
2891            from vedo import *
2892            xcoords = np.arange(0, 2, 0.2)
2893            ycoords = np.arange(0, 1, 0.2)
2894            sqrtx = sqrt(xcoords)
2895            grid = Grid(s=(sqrtx, ycoords)).lw(2)
2896            grid.show(axes=8).close()
2897
2898            # Can also create a grid from a np.mgrid:
2899            X, Y = np.mgrid[-12:12:10*1j, 200:215:10*1j]
2900            vgrid = Grid(s=(X[:,0], Y[0]))
2901            vgrid.show(axes=8).close()
2902            ```
2903            ![](https://vedo.embl.es/images/feats/uneven_grid.png)
2904        """
2905        resx, resy = res
2906        sx, sy = s
2907        
2908        try:
2909            bb = pos.bounds()
2910            pos = [(bb[0] + bb[1])/2, (bb[2] + bb[3])/2, (bb[4] + bb[5])/2]
2911            sx = bb[1] - bb[0]
2912            sy = bb[3] - bb[2]
2913        except AttributeError:
2914            pass        
2915
2916        if len(pos) == 2:
2917            pos = (pos[0], pos[1], 0)
2918        elif len(pos) in [4,6]: # passing a bounding box
2919            bb = pos
2920            pos = [(bb[0] + bb[1])/2, (bb[2] + bb[3])/2, 0]
2921            sx = bb[1] - bb[0]
2922            sy = bb[3] - bb[2]
2923            if len(pos)==6:
2924                pos[2] = bb[4] - bb[5]
2925
2926        if utils.is_sequence(sx) and utils.is_sequence(sy):
2927            verts = []
2928            for y in sy:
2929                for x in sx:
2930                    verts.append([x, y, 0])
2931            faces = []
2932            n = len(sx)
2933            m = len(sy)
2934            for j in range(m - 1):
2935                j1n = (j + 1) * n
2936                for i in range(n - 1):
2937                    faces.append([i + j * n, i + 1 + j * n, i + 1 + j1n, i + j1n])
2938
2939            super().__init__([verts, faces], c, alpha)
2940
2941        else:
2942            ps = vtki.new("PlaneSource")
2943            ps.SetResolution(resx, resy)
2944            ps.Update()
2945
2946            t = vtki.vtkTransform()
2947            t.Translate(pos)
2948            t.Scale(sx, sy, 1)
2949
2950            tf = vtki.new("TransformPolyDataFilter")
2951            tf.SetInputData(ps.GetOutput())
2952            tf.SetTransform(t)
2953            tf.Update()
2954
2955            super().__init__(tf.GetOutput(), c, alpha)
2956
2957        self.wireframe().lw(lw)
2958        self.properties.LightingOff()
2959        self.name = "Grid"
2960
2961
2962class Plane(Mesh):
2963    """Create a plane in space."""
2964
2965    def __init__(
2966            self,
2967            pos=(0, 0, 0),
2968            normal=(0, 0, 1),
2969            s=(1, 1),
2970            res=(1, 1),
2971            c="gray5", alpha=1.0,
2972        ) -> None:
2973        """
2974        Create a plane of size `s=(xsize, ysize)` oriented perpendicular
2975        to vector `normal` so that it passes through point `pos`.
2976
2977        Arguments:
2978            pos : (list)
2979                position of the plane center
2980            normal : (list)
2981                normal vector to the plane
2982            s : (list)
2983                size of the plane along x and y
2984            res : (list)
2985                resolution of the plane along x and y
2986        """
2987        if isinstance(pos, vtki.vtkPolyData):
2988            super().__init__(pos, c, alpha)
2989            # self.transform = LinearTransform().translate(pos)
2990
2991        else:
2992            ps = vtki.new("PlaneSource")
2993            ps.SetResolution(res[0], res[1])
2994            tri = vtki.new("TriangleFilter")
2995            tri.SetInputConnection(ps.GetOutputPort())
2996            tri.Update()
2997            
2998            super().__init__(tri.GetOutput(), c, alpha)
2999
3000            pos = utils.make3d(pos)
3001            normal = np.asarray(normal, dtype=float)
3002            axis = normal / np.linalg.norm(normal)
3003            theta = np.arccos(axis[2])
3004            phi = np.arctan2(axis[1], axis[0])
3005
3006            t = LinearTransform()
3007            t.scale([s[0], s[1], 1])
3008            t.rotate_y(np.rad2deg(theta))
3009            t.rotate_z(np.rad2deg(phi))
3010            t.translate(pos)
3011            self.apply_transform(t)
3012
3013        self.lighting("off")
3014        self.name = "Plane"
3015        self.variance = 0
3016
3017    def clone(self, deep=True) -> "Plane":
3018        newplane = Plane()
3019        if deep:
3020            newplane.dataset.DeepCopy(self.dataset)
3021        else:
3022            newplane.dataset.ShallowCopy(self.dataset)
3023        newplane.copy_properties_from(self)
3024        newplane.transform = self.transform.clone()
3025        newplane.variance = 0
3026        return newplane
3027    
3028    @property
3029    def normal(self) -> np.ndarray:
3030        pts = self.vertices
3031        AB = pts[1] - pts[0]
3032        AC = pts[2] - pts[0]
3033        normal = np.cross(AB, AC)
3034        normal = normal / np.linalg.norm(normal)
3035        return normal
3036
3037    @property
3038    def center(self) -> np.ndarray:
3039        pts = self.vertices
3040        return np.mean(pts, axis=0)
3041
3042    def contains(self, points, tol=0) -> np.ndarray:
3043        """
3044        Check if each of the provided point lies on this plane.
3045        `points` is an array of shape (n, 3).
3046        """
3047        points = np.array(points, dtype=float)
3048        bounds = self.vertices
3049
3050        mask = np.isclose(np.dot(points - self.center, self.normal), 0, atol=tol)
3051
3052        for i in [1, 3]:
3053            AB = bounds[i] - bounds[0]
3054            AP = points - bounds[0]
3055            mask_l = np.less_equal(np.dot(AP, AB), np.linalg.norm(AB))
3056            mask_g = np.greater_equal(np.dot(AP, AB), 0)
3057            mask = np.logical_and(mask, mask_l)
3058            mask = np.logical_and(mask, mask_g)
3059        return mask
3060
3061
3062class Rectangle(Mesh):
3063    """
3064    Build a rectangle in the xy plane.
3065    """
3066
3067    def __init__(self, p1=(0, 0), p2=(1, 1), radius=None, res=12, c="gray5", alpha=1.0) -> None:
3068        """
3069        Build a rectangle in the xy plane identified by any two corner points.
3070
3071        Arguments:
3072            p1 : (list)
3073                bottom-left position of the corner
3074            p2 : (list)
3075                top-right position of the corner
3076            radius : (float, list)
3077                smoothing radius of the corner in world units.
3078                A list can be passed with 4 individual values.
3079        """
3080        if len(p1) == 2:
3081            p1 = np.array([p1[0], p1[1], 0.0])
3082        else:
3083            p1 = np.array(p1, dtype=float)
3084        if len(p2) == 2:
3085            p2 = np.array([p2[0], p2[1], 0.0])
3086        else:
3087            p2 = np.array(p2, dtype=float)
3088
3089        self.corner1 = p1
3090        self.corner2 = p2
3091
3092        color = c
3093        smoothr = False
3094        risseq = False
3095        if utils.is_sequence(radius):
3096            risseq = True
3097            smoothr = True
3098            if max(radius) == 0:
3099                smoothr = False
3100        elif radius:
3101            smoothr = True
3102
3103        if not smoothr:
3104            radius = None
3105        self.radius = radius
3106
3107        if smoothr:
3108            r = radius
3109            if not risseq:
3110                r = [r, r, r, r]
3111            rd, ra, rb, rc = r
3112
3113            if p1[0] > p2[0]:  # flip p1 - p2
3114                p1, p2 = p2, p1
3115            if p1[1] > p2[1]:  # flip p1y - p2y
3116                p1[1], p2[1] = p2[1], p1[1]
3117
3118            px, py, _ = p2 - p1
3119            k = min(px / 2, py / 2)
3120            ra = min(abs(ra), k)
3121            rb = min(abs(rb), k)
3122            rc = min(abs(rc), k)
3123            rd = min(abs(rd), k)
3124            beta = np.linspace(0, 2 * np.pi, num=res * 4, endpoint=False)
3125            betas = np.split(beta, 4)
3126            rrx = np.cos(betas)
3127            rry = np.sin(betas)
3128
3129            q1 = (rd, 0)
3130            # q2 = (px-ra, 0)
3131            q3 = (px, ra)
3132            # q4 = (px, py-rb)
3133            q5 = (px - rb, py)
3134            # q6 = (rc, py)
3135            q7 = (0, py - rc)
3136            # q8 = (0, rd)
3137            a = np.c_[rrx[3], rry[3]]*ra + [px-ra, ra]    if ra else np.array([])
3138            b = np.c_[rrx[0], rry[0]]*rb + [px-rb, py-rb] if rb else np.array([])
3139            c = np.c_[rrx[1], rry[1]]*rc + [rc, py-rc]    if rc else np.array([])
3140            d = np.c_[rrx[2], rry[2]]*rd + [rd, rd]       if rd else np.array([])
3141
3142            pts = [q1, *a.tolist(), q3, *b.tolist(), q5, *c.tolist(), q7, *d.tolist()]
3143            faces = [list(range(len(pts)))]
3144        else:
3145            p1r = np.array([p2[0], p1[1], 0.0])
3146            p2l = np.array([p1[0], p2[1], 0.0])
3147            pts = ([0.0, 0.0, 0.0], p1r - p1, p2 - p1, p2l - p1)
3148            faces = [(0, 1, 2, 3)]
3149
3150        super().__init__([pts, faces], color, alpha)
3151        self.pos(p1)
3152        self.properties.LightingOff()
3153        self.name = "Rectangle"
3154
3155
3156class Box(Mesh):
3157    """
3158    Build a box of specified dimensions.
3159    """
3160
3161    def __init__(
3162            self, pos=(0, 0, 0), 
3163            length=1.0, width=2.0, height=3.0, size=(), c="g4", alpha=1.0) -> None:
3164        """
3165        Build a box of dimensions `x=length, y=width and z=height`.
3166        Alternatively dimensions can be defined by setting `size` keyword with a tuple.
3167
3168        If `pos` is a list of 6 numbers, this will be interpreted as the bounding box:
3169        `[xmin,xmax, ymin,ymax, zmin,zmax]`
3170
3171        Examples:
3172            - [aspring1.py](https://github.com/marcomusy/vedo/tree/master/examples/simulations/aspring1.py)
3173
3174                ![](https://vedo.embl.es/images/simulations/50738955-7e891800-11d9-11e9-85cd-02bd4f3f13ea.gif)
3175        """
3176        src = vtki.new("CubeSource")
3177
3178        if len(pos) == 2:
3179            pos = (pos[0], pos[1], 0)
3180
3181        if len(pos) == 6:
3182            src.SetBounds(pos)
3183            pos = [(pos[0] + pos[1]) / 2, (pos[2] + pos[3]) / 2, (pos[4] + pos[5]) / 2]
3184        elif len(size) == 3:
3185            length, width, height = size
3186            src.SetXLength(length)
3187            src.SetYLength(width)
3188            src.SetZLength(height)
3189            src.SetCenter(pos)
3190        else:
3191            src.SetXLength(length)
3192            src.SetYLength(width)
3193            src.SetZLength(height)
3194            src.SetCenter(pos)
3195
3196        src.Update()
3197        pd = src.GetOutput()
3198
3199        tc = [
3200            [0.0, 0.0],
3201            [1.0, 0.0],
3202            [0.0, 1.0],
3203            [1.0, 1.0],
3204            [1.0, 0.0],
3205            [0.0, 0.0],
3206            [1.0, 1.0],
3207            [0.0, 1.0],
3208            [1.0, 1.0],
3209            [1.0, 0.0],
3210            [0.0, 1.0],
3211            [0.0, 0.0],
3212            [0.0, 1.0],
3213            [0.0, 0.0],
3214            [1.0, 1.0],
3215            [1.0, 0.0],
3216            [1.0, 0.0],
3217            [0.0, 0.0],
3218            [1.0, 1.0],
3219            [0.0, 1.0],
3220            [0.0, 0.0],
3221            [1.0, 0.0],
3222            [0.0, 1.0],
3223            [1.0, 1.0],
3224        ]
3225        vtc = utils.numpy2vtk(tc)
3226        pd.GetPointData().SetTCoords(vtc)
3227        super().__init__(pd, c, alpha)
3228        self.transform = LinearTransform().translate(pos)
3229        self.name = "Box"
3230
3231
3232class Cube(Box):
3233    """Build a cube."""
3234
3235    def __init__(self, pos=(0, 0, 0), side=1.0, c="g4", alpha=1.0) -> None:
3236        """Build a cube of size `side`."""
3237        super().__init__(pos, side, side, side, (), c, alpha)
3238        self.name = "Cube"
3239
3240
3241class TessellatedBox(Mesh):
3242    """
3243    Build a cubic `Mesh` made of quads.
3244    """
3245
3246    def __init__(self, pos=(0, 0, 0), n=10, spacing=(1, 1, 1), bounds=(), c="k5", alpha=0.5) -> None:
3247        """
3248        Build a cubic `Mesh` made of `n` small quads in the 3 axis directions.
3249
3250        Arguments:
3251            pos : (list)
3252                position of the left bottom corner
3253            n : (int, list)
3254                number of subdivisions along each side
3255            spacing : (float)
3256                size of the side of the single quad in the 3 directions
3257        """
3258        if utils.is_sequence(n):  # slow
3259            img = vtki.vtkImageData()
3260            img.SetDimensions(n[0] + 1, n[1] + 1, n[2] + 1)
3261            img.SetSpacing(spacing)
3262            gf = vtki.new("GeometryFilter")
3263            gf.SetInputData(img)
3264            gf.Update()
3265            poly = gf.GetOutput()
3266        else:  # fast
3267            n -= 1
3268            tbs = vtki.new("TessellatedBoxSource")
3269            tbs.SetLevel(n)
3270            if len(bounds):
3271                tbs.SetBounds(bounds)
3272            else:
3273                tbs.SetBounds(0, n * spacing[0], 0, n * spacing[1], 0, n * spacing[2])
3274            tbs.QuadsOn()
3275            #tbs.SetOutputPointsPrecision(vtki.vtkAlgorithm.SINGLE_PRECISION)
3276            tbs.Update()
3277            poly = tbs.GetOutput()
3278        super().__init__(poly, c=c, alpha=alpha)
3279        self.pos(pos)
3280        self.lw(1).lighting("off")
3281        self.name = "TessellatedBox"
3282
3283
3284class Spring(Mesh):
3285    """
3286    Build a spring model.
3287    """
3288
3289    def __init__(
3290        self,
3291        start_pt=(0, 0, 0),
3292        end_pt=(1, 0, 0),
3293        coils=20,
3294        r1=0.1,
3295        r2=None,
3296        thickness=None,
3297        c="gray5",
3298        alpha=1.0,
3299    ) -> None:
3300        """
3301        Build a spring of specified nr of `coils` between `start_pt` and `end_pt`.
3302
3303        Arguments:
3304            coils : (int)
3305                number of coils
3306            r1 : (float)
3307                radius at start point
3308            r2 : (float)
3309                radius at end point
3310            thickness : (float)
3311                thickness of the coil section
3312        """
3313        start_pt = utils.make3d(start_pt)
3314        end_pt = utils.make3d(end_pt)
3315
3316        diff = end_pt - start_pt
3317        length = np.linalg.norm(diff)
3318        if not length:
3319            return
3320        if not r1:
3321            r1 = length / 20
3322        trange = np.linspace(0, length, num=50 * coils)
3323        om = 6.283 * (coils - 0.5) / length
3324        if not r2:
3325            r2 = r1
3326        pts = []
3327        for t in trange:
3328            f = (length - t) / length
3329            rd = r1 * f + r2 * (1 - f)
3330            pts.append([rd * np.cos(om * t), rd * np.sin(om * t), t])
3331
3332        pts = [[0, 0, 0]] + pts + [[0, 0, length]]
3333        diff = diff / length
3334        theta = np.arccos(diff[2])
3335        phi = np.arctan2(diff[1], diff[0])
3336        sp = Line(pts)
3337        
3338        t = vtki.vtkTransform()
3339        t.Translate(start_pt)
3340        t.RotateZ(np.rad2deg(phi))
3341        t.RotateY(np.rad2deg(theta))
3342
3343        tf = vtki.new("TransformPolyDataFilter")
3344        tf.SetInputData(sp.dataset)
3345        tf.SetTransform(t)
3346        tf.Update()
3347
3348        tuf = vtki.new("TubeFilter")
3349        tuf.SetNumberOfSides(12)
3350        tuf.CappingOn()
3351        tuf.SetInputData(tf.GetOutput())
3352        if not thickness:
3353            thickness = r1 / 10
3354        tuf.SetRadius(thickness)
3355        tuf.Update()
3356
3357        super().__init__(tuf.GetOutput(), c, alpha)
3358
3359        self.phong()
3360        self.base = np.array(start_pt, dtype=float)
3361        self.top  = np.array(end_pt, dtype=float)
3362        self.name = "Spring"
3363
3364
3365class Cylinder(Mesh):
3366    """
3367    Build a cylinder of specified height and radius.
3368    """
3369
3370    def __init__(
3371        self, pos=(0, 0, 0), r=1.0, height=2.0, axis=(0, 0, 1),
3372        cap=True, res=24, c="teal3", alpha=1.0
3373    ) -> None:
3374        """
3375        Build a cylinder of specified height and radius `r`, centered at `pos`.
3376
3377        If `pos` is a list of 2 points, e.g. `pos=[v1, v2]`, build a cylinder with base
3378        centered at `v1` and top at `v2`.
3379
3380        Arguments:
3381            cap : (bool)
3382                enable/disable the caps of the cylinder
3383            res : (int)
3384                resolution of the cylinder sides
3385
3386        ![](https://raw.githubusercontent.com/lorensen/VTKExamples/master/src/Testing/Baseline/Cxx/GeometricObjects/TestCylinder.png)
3387        """
3388        if utils.is_sequence(pos[0]):  # assume user is passing pos=[base, top]
3389            base = np.array(pos[0], dtype=float)
3390            top = np.array(pos[1], dtype=float)
3391            pos = (base + top) / 2
3392            height = np.linalg.norm(top - base)
3393            axis = top - base
3394            axis = utils.versor(axis)
3395        else:
3396            axis = utils.versor(axis)
3397            base = pos - axis * height / 2
3398            top = pos + axis * height / 2
3399
3400        cyl = vtki.new("CylinderSource")
3401        cyl.SetResolution(res)
3402        cyl.SetRadius(r)
3403        cyl.SetHeight(height)
3404        cyl.SetCapping(cap)
3405        cyl.Update()
3406
3407        theta = np.arccos(axis[2])
3408        phi = np.arctan2(axis[1], axis[0])
3409        t = vtki.vtkTransform()
3410        t.PostMultiply()
3411        t.RotateX(90)  # put it along Z
3412        t.RotateY(np.rad2deg(theta))
3413        t.RotateZ(np.rad2deg(phi))
3414        t.Translate(pos)
3415
3416        tf = vtki.new("TransformPolyDataFilter")
3417        tf.SetInputData(cyl.GetOutput())
3418        tf.SetTransform(t)
3419        tf.Update()
3420
3421        super().__init__(tf.GetOutput(), c, alpha)
3422
3423        self.phong()
3424        self.base = base
3425        self.top  = top
3426        self.transform = LinearTransform().translate(pos)
3427        self.name = "Cylinder"
3428
3429
3430class Cone(Mesh):
3431    """Build a cone of specified radius and height."""
3432
3433    def __init__(self, pos=(0, 0, 0), r=1.0, height=3.0, axis=(0, 0, 1),
3434                 res=48, c="green3", alpha=1.0) -> None:
3435        """Build a cone of specified radius `r` and `height`, centered at `pos`."""
3436        con = vtki.new("ConeSource")
3437        con.SetResolution(res)
3438        con.SetRadius(r)
3439        con.SetHeight(height)
3440        con.SetDirection(axis)
3441        con.Update()
3442        super().__init__(con.GetOutput(), c, alpha)
3443        self.phong()
3444        if len(pos) == 2:
3445            pos = (pos[0], pos[1], 0)
3446        self.pos(pos)
3447        v = utils.versor(axis) * height / 2
3448        self.base = pos - v
3449        self.top  = pos + v
3450        self.name = "Cone"
3451
3452
3453class Pyramid(Cone):
3454    """Build a pyramidal shape."""
3455
3456    def __init__(self, pos=(0, 0, 0), s=1.0, height=1.0, axis=(0, 0, 1),
3457                 c="green3", alpha=1) -> None:
3458        """Build a pyramid of specified base size `s` and `height`, centered at `pos`."""
3459        super().__init__(pos, s, height, axis, 4, c, alpha)
3460        self.name = "Pyramid"
3461
3462
3463class Torus(Mesh):
3464    """
3465    Build a toroidal shape.
3466    """
3467
3468    def __init__(self, pos=(0, 0, 0), r1=1.0, r2=0.2, res=36, quads=False, c="yellow3", alpha=1.0) -> None:
3469        """
3470        Build a torus of specified outer radius `r1` internal radius `r2`, centered at `pos`.
3471        If `quad=True` a quad-mesh is generated.
3472        """
3473        if utils.is_sequence(res):
3474            res_u, res_v = res
3475        else:
3476            res_u, res_v = 3 * res, res
3477
3478        if quads:
3479            # https://github.com/marcomusy/vedo/issues/710
3480
3481            n = res_v
3482            m = res_u
3483
3484            theta = np.linspace(0, 2.0 * np.pi, n)
3485            phi = np.linspace(0, 2.0 * np.pi, m)
3486            theta, phi = np.meshgrid(theta, phi)
3487            t = r1 + r2 * np.cos(theta)
3488            x = t * np.cos(phi)
3489            y = t * np.sin(phi)
3490            z = r2 * np.sin(theta)
3491            pts = np.column_stack((x.ravel(), y.ravel(), z.ravel()))
3492
3493            faces = []
3494            for j in range(m - 1):
3495                j1n = (j + 1) * n
3496                for i in range(n - 1):
3497                    faces.append([i + j * n, i + 1 + j * n, i + 1 + j1n, i + j1n])
3498
3499            super().__init__([pts, faces], c, alpha)
3500
3501        else:
3502            rs = vtki.new("ParametricTorus")
3503            rs.SetRingRadius(r1)
3504            rs.SetCrossSectionRadius(r2)
3505            pfs = vtki.new("ParametricFunctionSource")
3506            pfs.SetParametricFunction(rs)
3507            pfs.SetUResolution(res_u)
3508            pfs.SetVResolution(res_v)
3509            pfs.Update()
3510
3511            super().__init__(pfs.GetOutput(), c, alpha)
3512
3513        self.phong()
3514        if len(pos) == 2:
3515            pos = (pos[0], pos[1], 0)
3516        self.pos(pos)
3517        self.name = "Torus"
3518
3519
3520class Paraboloid(Mesh):
3521    """
3522    Build a paraboloid.
3523    """
3524
3525    def __init__(self, pos=(0, 0, 0), height=1.0, res=50, c="cyan5", alpha=1.0) -> None:
3526        """
3527        Build a paraboloid of specified height and radius `r`, centered at `pos`.
3528
3529        Full volumetric expression is:
3530            `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`
3531
3532        ![](https://user-images.githubusercontent.com/32848391/51211547-260ef480-1916-11e9-95f6-4a677e37e355.png)
3533        """
3534        quadric = vtki.new("Quadric")
3535        quadric.SetCoefficients(1, 1, 0, 0, 0, 0, 0, 0, height / 4, 0)
3536        # F(x,y,z) = a0*x^2 + a1*y^2 + a2*z^2
3537        #         + a3*x*y + a4*y*z + a5*x*z
3538        #         + a6*x   + a7*y   + a8*z  +a9
3539        sample = vtki.new("SampleFunction")
3540        sample.SetSampleDimensions(res, res, res)
3541        sample.SetImplicitFunction(quadric)
3542
3543        contours = vtki.new("ContourFilter")
3544        contours.SetInputConnection(sample.GetOutputPort())
3545        contours.GenerateValues(1, 0.01, 0.01)
3546        contours.Update()
3547
3548        super().__init__(contours.GetOutput(), c, alpha)
3549        self.compute_normals().phong()
3550        self.mapper.ScalarVisibilityOff()
3551        self.pos(pos)
3552        self.name = "Paraboloid"
3553
3554
3555class Hyperboloid(Mesh):
3556    """
3557    Build a hyperboloid.
3558    """
3559
3560    def __init__(self, pos=(0, 0, 0), a2=1.0, value=0.5, res=100, c="pink4", alpha=1.0) -> None:
3561        """
3562        Build a hyperboloid of specified aperture `a2` and `height`, centered at `pos`.
3563
3564        Full volumetric expression is:
3565            `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`
3566        """
3567        q = vtki.new("Quadric")
3568        q.SetCoefficients(2, 2, -1 / a2, 0, 0, 0, 0, 0, 0, 0)
3569        # F(x,y,z) = a0*x^2 + a1*y^2 + a2*z^2
3570        #         + a3*x*y + a4*y*z + a5*x*z
3571        #         + a6*x   + a7*y   + a8*z  +a9
3572        sample = vtki.new("SampleFunction")
3573        sample.SetSampleDimensions(res, res, res)
3574        sample.SetImplicitFunction(q)
3575
3576        contours = vtki.new("ContourFilter")
3577        contours.SetInputConnection(sample.GetOutputPort())
3578        contours.GenerateValues(1, value, value)
3579        contours.Update()
3580
3581        super().__init__(contours.GetOutput(), c, alpha)
3582        self.compute_normals().phong()
3583        self.mapper.ScalarVisibilityOff()
3584        self.pos(pos)
3585        self.name = "Hyperboloid"
3586
3587
3588def Marker(symbol, pos=(0, 0, 0), c="k", alpha=1.0, s=0.1, filled=True) -> Any:
3589    """
3590    Generate a marker shape. Typically used in association with `Glyph`.
3591    """
3592    if isinstance(symbol, Mesh):
3593        return symbol.c(c).alpha(alpha).lighting("off")
3594
3595    if isinstance(symbol, int):
3596        symbs = [".", "o", "O", "0", "p", "*", "h", "D", "d", "v", "^", ">", "<", "s", "x", "a"]
3597        symbol = symbol % len(symbs)
3598        symbol = symbs[symbol]
3599
3600    if symbol == ".":
3601        mesh = Polygon(nsides=24, r=s * 0.6)
3602    elif symbol == "o":
3603        mesh = Polygon(nsides=24, r=s * 0.75)
3604    elif symbol == "O":
3605        mesh = Disc(r1=s * 0.6, r2=s * 0.75, res=(1, 24))
3606    elif symbol == "0":
3607        m1 = Disc(r1=s * 0.6, r2=s * 0.75, res=(1, 24))
3608        m2 = Circle(r=s * 0.36).reverse()
3609        mesh = merge(m1, m2)
3610    elif symbol == "p":
3611        mesh = Polygon(nsides=5, r=s)
3612    elif symbol == "*":
3613        mesh = Star(r1=0.65 * s * 1.1, r2=s * 1.1, line=not filled)
3614    elif symbol == "h":
3615        mesh = Polygon(nsides=6, r=s)
3616    elif symbol == "D":
3617        mesh = Polygon(nsides=4, r=s)
3618    elif symbol == "d":
3619        mesh = Polygon(nsides=4, r=s * 1.1).scale([0.5, 1, 1])
3620    elif symbol == "v":
3621        mesh = Polygon(nsides=3, r=s).rotate_z(180)
3622    elif symbol == "^":
3623        mesh = Polygon(nsides=3, r=s)
3624    elif symbol == ">":
3625        mesh = Polygon(nsides=3, r=s).rotate_z(-90)
3626    elif symbol == "<":
3627        mesh = Polygon(nsides=3, r=s).rotate_z(90)
3628    elif symbol == "s":
3629        mesh = Mesh(
3630            [[[-1, -1, 0], [1, -1, 0], [1, 1, 0], [-1, 1, 0]], [[0, 1, 2, 3]]]
3631        ).scale(s / 1.4)
3632    elif symbol == "x":
3633        mesh = Text3D("+", pos=(0, 0, 0), s=s * 2.6, justify="center", depth=0)
3634        # mesh.rotate_z(45)
3635    elif symbol == "a":
3636        mesh = Text3D("*", pos=(0, 0, 0), s=s * 2.6, justify="center", depth=0)
3637    else:
3638        mesh = Text3D(symbol, pos=(0, 0, 0), s=s * 2, justify="center", depth=0)
3639    mesh.flat().lighting("off").wireframe(not filled).c(c).alpha(alpha)
3640    if len(pos) == 2:
3641        pos = (pos[0], pos[1], 0)
3642    mesh.pos(pos)
3643    mesh.name = "Marker"
3644    return mesh
3645
3646
3647class Brace(Mesh):
3648    """
3649    Create a brace (bracket) shape.
3650    """
3651
3652    def __init__(
3653        self,
3654        q1,
3655        q2,
3656        style="}",
3657        padding1=0.0,
3658        font="Theemim",
3659        comment="",
3660        justify=None,
3661        angle=0.0,
3662        padding2=0.2,
3663        s=1.0,
3664        italic=0,
3665        c="k1",
3666        alpha=1.0,
3667    ) -> None:
3668        """
3669        Create a brace (bracket) shape which spans from point q1 to point q2.
3670
3671        Arguments:
3672            q1 : (list)
3673                point 1.
3674            q2 : (list)
3675                point 2.
3676            style : (str)
3677                style of the bracket, eg. `{}, [], (), <>`.
3678            padding1 : (float)
3679                padding space in percent form the input points.
3680            font : (str)
3681                font type
3682            comment : (str)
3683                additional text to appear next to the brace symbol.
3684            justify : (str)
3685                specify the anchor point to justify text comment, e.g. "top-left".
3686            italic : float
3687                italicness of the text comment (can be a positive or negative number)
3688            angle : (float)
3689                rotation angle of text. Use `None` to keep it horizontal.
3690            padding2 : (float)
3691                padding space in percent form brace to text comment.
3692            s : (float)
3693                scale factor for the comment
3694
3695        Examples:
3696            - [scatter3.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/scatter3.py)
3697
3698                ![](https://vedo.embl.es/images/pyplot/scatter3.png)
3699        """
3700        if isinstance(q1, vtki.vtkActor):
3701            q1 = q1.GetPosition()
3702        if isinstance(q2, vtki.vtkActor):
3703            q2 = q2.GetPosition()
3704        if len(q1) == 2:
3705            q1 = [q1[0], q1[1], 0.0]
3706        if len(q2) == 2:
3707            q2 = [q2[0], q2[1], 0.0]
3708        q1 = np.array(q1, dtype=float)
3709        q2 = np.array(q2, dtype=float)
3710        mq = (q1 + q2) / 2
3711        q1 = q1 - mq
3712        q2 = q2 - mq
3713        d = np.linalg.norm(q2 - q1)
3714        q2[2] = q1[2]
3715
3716        if style not in "{}[]()<>|I":
3717            vedo.logger.error(f"unknown style {style}." + "Use {}[]()<>|I")
3718            style = "}"
3719
3720        flip = False
3721        if style in ["{", "[", "(", "<"]:
3722            flip = True
3723            i = ["{", "[", "(", "<"].index(style)
3724            style = ["}", "]", ")", ">"][i]
3725
3726        br = Text3D(style, font="Theemim", justify="center-left")
3727        br.scale([0.4, 1, 1])
3728
3729        angler = np.arctan2(q2[1], q2[0]) * 180 / np.pi - 90
3730        if flip:
3731            angler += 180
3732
3733        _, x1, y0, y1, _, _ = br.bounds()
3734        if comment:
3735            just = "center-top"
3736            if angle is None:
3737                angle = -angler + 90
3738                if not flip:
3739                    angle += 180
3740
3741            if flip:
3742                angle += 180
3743                just = "center-bottom"
3744            if justify is not None:
3745                just = justify
3746            cmt = Text3D(comment, font=font, justify=just, italic=italic)
3747            cx0, cx1 = cmt.xbounds()
3748            cmt.rotate_z(90 + angle)
3749            cmt.scale(1 / (cx1 - cx0) * s * len(comment) / 5)
3750            cmt.shift(x1 * (1 + padding2), 0, 0)
3751            poly = merge(br, cmt).dataset
3752
3753        else:
3754            poly = br.dataset
3755
3756        tr = vtki.vtkTransform()
3757        tr.Translate(mq)
3758        tr.RotateZ(angler)
3759        tr.Translate(padding1 * d, 0, 0)
3760        pscale = 1
3761        tr.Scale(pscale / (y1 - y0) * d, pscale / (y1 - y0) * d, 1)
3762
3763        tf = vtki.new("TransformPolyDataFilter")
3764        tf.SetInputData(poly)
3765        tf.SetTransform(tr)
3766        tf.Update()
3767        poly = tf.GetOutput()
3768
3769        super().__init__(poly, c, alpha)
3770
3771        self.base = q1
3772        self.top  = q2
3773        self.name = "Brace"
3774
3775
3776class Star3D(Mesh):
3777    """
3778    Build a 3D starred shape.
3779    """
3780
3781    def __init__(self, pos=(0, 0, 0), r=1.0, thickness=0.1, c="blue4", alpha=1.0) -> None:
3782        """
3783        Build a 3D star shape of 5 cusps, mainly useful as a 3D marker.
3784        """
3785        pts = ((1.34, 0., -0.37), (5.75e-3, -0.588, thickness/10), (0.377, 0.,-0.38),
3786               (0.0116, 0., -1.35), (-0.366, 0., -0.384), (-1.33, 0., -0.385),
3787               (-0.600, 0., 0.321), (-0.829, 0., 1.19), (-1.17e-3, 0., 0.761),
3788               (0.824, 0., 1.20), (0.602, 0., 0.328), (6.07e-3, 0.588, thickness/10))
3789        fcs = [[0, 1, 2], [0, 11,10], [2, 1, 3], [2, 11, 0], [3, 1, 4], [3, 11, 2],
3790               [4, 1, 5], [4, 11, 3], [5, 1, 6], [5, 11, 4], [6, 1, 7], [6, 11, 5],
3791               [7, 1, 8], [7, 11, 6], [8, 1, 9], [8, 11, 7], [9, 1,10], [9, 11, 8],
3792               [10,1, 0],[10,11, 9]]
3793
3794        super().__init__([pts, fcs], c, alpha)
3795        self.rotate_x(90)
3796        self.scale(r).lighting("shiny")
3797
3798        if len(pos) == 2:
3799            pos = (pos[0], pos[1], 0)
3800        self.pos(pos)
3801        self.name = "Star3D"
3802
3803
3804class Cross3D(Mesh):
3805    """
3806    Build a 3D cross shape.
3807    """
3808
3809    def __init__(self, pos=(0, 0, 0), s=1.0, thickness=0.3, c="b", alpha=1.0) -> None:
3810        """
3811        Build a 3D cross shape, mainly useful as a 3D marker.
3812        """
3813        if len(pos) == 2:
3814            pos = (pos[0], pos[1], 0)
3815
3816        c1 = Cylinder(r=thickness * s, height=2 * s)
3817        c2 = Cylinder(r=thickness * s, height=2 * s).rotate_x(90)
3818        c3 = Cylinder(r=thickness * s, height=2 * s).rotate_y(90)
3819        poly = merge(c1, c2, c3).color(c).alpha(alpha).pos(pos).dataset
3820        super().__init__(poly, c, alpha)
3821        self.name = "Cross3D"
3822
3823
3824class ParametricShape(Mesh):
3825    """
3826    A set of built-in shapes mainly for illustration purposes.
3827    """
3828
3829    def __init__(self, name, res=51, n=25, seed=1):
3830        """
3831        A set of built-in shapes mainly for illustration purposes.
3832
3833        Name can be an integer or a string in this list:
3834            `['Boy', 'ConicSpiral', 'CrossCap', 'Dini', 'Enneper',
3835            'Figure8Klein', 'Klein', 'Mobius', 'RandomHills', 'Roman',
3836            'SuperEllipsoid', 'BohemianDome', 'Bour', 'CatalanMinimal',
3837            'Henneberg', 'Kuen', 'PluckerConoid', 'Pseudosphere']`.
3838
3839        Example:
3840            ```python
3841            from vedo import *
3842            settings.immediate_rendering = False
3843            plt = Plotter(N=18)
3844            for i in range(18):
3845                ps = ParametricShape(i).color(i)
3846                plt.at(i).show(ps, ps.name)
3847            plt.interactive().close()
3848            ```
3849            <img src="https://user-images.githubusercontent.com/32848391/69181075-bb6aae80-0b0e-11ea-92f7-d0cd3b9087bf.png" width="700">
3850        """
3851
3852        shapes = [
3853            "Boy",
3854            "ConicSpiral",
3855            "CrossCap",
3856            "Enneper",
3857            "Figure8Klein",
3858            "Klein",
3859            "Dini",
3860            "Mobius",
3861            "RandomHills",
3862            "Roman",
3863            "SuperEllipsoid",
3864            "BohemianDome",
3865            "Bour",
3866            "CatalanMinimal",
3867            "Henneberg",
3868            "Kuen",
3869            "PluckerConoid",
3870            "Pseudosphere",
3871        ]
3872
3873        if isinstance(name, int):
3874            name = name % len(shapes)
3875            name = shapes[name]
3876
3877        if name == "Boy":
3878            ps = vtki.new("ParametricBoy")
3879        elif name == "ConicSpiral":
3880            ps = vtki.new("ParametricConicSpiral")
3881        elif name == "CrossCap":
3882            ps = vtki.new("ParametricCrossCap")
3883        elif name == "Dini":
3884            ps = vtki.new("ParametricDini")
3885        elif name == "Enneper":
3886            ps = vtki.new("ParametricEnneper")
3887        elif name == "Figure8Klein":
3888            ps = vtki.new("ParametricFigure8Klein")
3889        elif name == "Klein":
3890            ps = vtki.new("ParametricKlein")
3891        elif name == "Mobius":
3892            ps = vtki.new("ParametricMobius")
3893            ps.SetRadius(2.0)
3894            ps.SetMinimumV(-0.5)
3895            ps.SetMaximumV(0.5)
3896        elif name == "RandomHills":
3897            ps = vtki.new("ParametricRandomHills")
3898            ps.AllowRandomGenerationOn()
3899            ps.SetRandomSeed(seed)
3900            ps.SetNumberOfHills(n)
3901        elif name == "Roman":
3902            ps = vtki.new("ParametricRoman")
3903        elif name == "SuperEllipsoid":
3904            ps = vtki.new("ParametricSuperEllipsoid")
3905            ps.SetN1(0.5)
3906            ps.SetN2(0.4)
3907        elif name == "BohemianDome":
3908            ps = vtki.new("ParametricBohemianDome")
3909            ps.SetA(5.0)
3910            ps.SetB(1.0)
3911            ps.SetC(2.0)
3912        elif name == "Bour":
3913            ps = vtki.new("ParametricBour")
3914        elif name == "CatalanMinimal":
3915            ps = vtki.new("ParametricCatalanMinimal")
3916        elif name == "Henneberg":
3917            ps = vtki.new("ParametricHenneberg")
3918        elif name == "Kuen":
3919            ps = vtki.new("ParametricKuen")
3920            ps.SetDeltaV0(0.001)
3921        elif name == "PluckerConoid":
3922            ps = vtki.new("ParametricPluckerConoid")
3923        elif name == "Pseudosphere":
3924            ps = vtki.new("ParametricPseudosphere")
3925        else:
3926            vedo.logger.error(f"unknown ParametricShape {name}")
3927            return
3928
3929        pfs = vtki.new("ParametricFunctionSource")
3930        pfs.SetParametricFunction(ps)
3931        pfs.SetUResolution(res)
3932        pfs.SetVResolution(res)
3933        pfs.SetWResolution(res)
3934        pfs.SetScalarModeToZ()
3935        pfs.Update()
3936
3937        super().__init__(pfs.GetOutput())
3938
3939        if name == "RandomHills": self.shift([0,-10,-2.25])
3940        if name != 'Kuen': self.normalize()
3941        if name == 'Dini': self.scale(0.4)
3942        if name == 'Enneper': self.scale(0.4)
3943        if name == 'ConicSpiral': self.bc('tomato')
3944        self.name = name
3945
3946
3947@lru_cache(None)
3948def _load_font(font) -> np.ndarray:
3949    # print('_load_font()', font)
3950
3951    if utils.is_number(font):
3952        font = list(settings.font_parameters.keys())[int(font)]
3953
3954    if font.endswith(".npz"):  # user passed font as a local path
3955        fontfile = font
3956        font = os.path.basename(font).split(".")[0]
3957
3958    elif font.startswith("https"):  # user passed URL link, make it a path
3959        try:
3960            fontfile = vedo.file_io.download(font, verbose=False, force=False)
3961            font = os.path.basename(font).split(".")[0]
3962        except:
3963            vedo.logger.warning(f"font {font} not found")
3964            font = settings.default_font
3965            fontfile = os.path.join(vedo.fonts_path, font + ".npz")
3966
3967    else:  # user passed font by its standard name
3968        font = font[:1].upper() + font[1:]  # capitalize first letter only
3969        fontfile = os.path.join(vedo.fonts_path, font + ".npz")
3970
3971        if font not in settings.font_parameters.keys():
3972            font = "Normografo"
3973            vedo.logger.warning(
3974                f"Unknown font: {font}\n"
3975                f"Available 3D fonts are: "
3976                f"{list(settings.font_parameters.keys())}\n"
3977                f"Using font {font} instead."
3978            )
3979            fontfile = os.path.join(vedo.fonts_path, font + ".npz")
3980
3981        if not settings.font_parameters[font]["islocal"]:
3982            font = "https://vedo.embl.es/fonts/" + font + ".npz"
3983            try:
3984                fontfile = vedo.file_io.download(font, verbose=False, force=False)
3985                font = os.path.basename(font).split(".")[0]
3986            except:
3987                vedo.logger.warning(f"font {font} not found")
3988                font = settings.default_font
3989                fontfile = os.path.join(vedo.fonts_path, font + ".npz")
3990
3991    #####
3992    try:
3993        font_meshes = np.load(fontfile, allow_pickle=True)["font"][0]
3994    except:
3995        vedo.logger.warning(f"font name {font} not found.")
3996        raise RuntimeError
3997    return font_meshes
3998
3999
4000@lru_cache(None)
4001def _get_font_letter(font, letter):
4002    # print("_get_font_letter", font, letter)
4003    font_meshes = _load_font(font)
4004    try:
4005        pts, faces = font_meshes[letter]
4006        return utils.buildPolyData(pts, faces)
4007    except KeyError:
4008        return None
4009
4010
4011class Text3D(Mesh):
4012    """
4013    Generate a 3D polygonal Mesh to represent a text string.
4014    """
4015
4016    def __init__(
4017        self,
4018        txt,
4019        pos=(0, 0, 0),
4020        s=1.0,
4021        font="",
4022        hspacing=1.15,
4023        vspacing=2.15,
4024        depth=0.0,
4025        italic=False,
4026        justify="bottom-left",
4027        literal=False,
4028        c=None,
4029        alpha=1.0,
4030    ) -> None:
4031        """
4032        Generate a 3D polygonal `Mesh` representing a text string.
4033
4034        Can render strings like `3.7 10^9` or `H_2 O` with subscripts and superscripts.
4035        Most Latex symbols are also supported.
4036
4037        Symbols `~ ^ _` are reserved modifiers:
4038        - use ~ to add a short space, 1/4 of the default empty space,
4039        - use ^ and _ to start up/sub scripting, a space terminates their effect.
4040
4041        Monospaced fonts are: `Calco, ComicMono, Glasgo, SmartCouric, VictorMono, Justino`.
4042
4043        More fonts at: https://vedo.embl.es/fonts/
4044
4045        Arguments:
4046            pos : (list)
4047                position coordinates in 3D space
4048            s : (float)
4049                vertical size of the text (as scaling factor)
4050            depth : (float)
4051                text thickness (along z)
4052            italic : (bool), float
4053                italic font type (can be a signed float too)
4054            justify : (str)
4055                text justification as centering of the bounding box
4056                (bottom-left, bottom-right, top-left, top-right, centered)
4057            font : (str, int)
4058                some of the available 3D-polygonized fonts are:
4059                Bongas, Calco, Comae, ComicMono, Kanopus, Glasgo, Ubuntu,
4060                LogoType, Normografo, Quikhand, SmartCouric, Theemim, VictorMono, VTK,
4061                Capsmall, Cartoons123, Vega, Justino, Spears, Meson.
4062
4063                Check for more at https://vedo.embl.es/fonts/
4064
4065                Or type in your terminal `vedo --run fonts`.
4066
4067                Default is Normografo, which can be changed using `settings.default_font`.
4068
4069            hspacing : (float)
4070                horizontal spacing of the font
4071            vspacing : (float)
4072                vertical spacing of the font for multiple lines text
4073            literal : (bool)
4074                if set to True will ignore modifiers like _ or ^
4075
4076        Examples:
4077            - [markpoint.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/markpoint.py)
4078            - [fonts.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/fonts.py)
4079            - [caption.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/caption.py)
4080
4081            ![](https://vedo.embl.es/images/pyplot/fonts3d.png)
4082
4083        .. note:: Type `vedo -r fonts` for a demo.
4084        """
4085        if len(pos) == 2:
4086            pos = (pos[0], pos[1], 0)
4087
4088        if c is None:  # automatic black or white
4089            pli = vedo.plotter_instance
4090            if pli and pli.renderer:
4091                c = (0.9, 0.9, 0.9)
4092                if pli.renderer.GetGradientBackground():
4093                    bgcol = pli.renderer.GetBackground2()
4094                else:
4095                    bgcol = pli.renderer.GetBackground()
4096                if np.sum(bgcol) > 1.5:
4097                    c = (0.1, 0.1, 0.1)
4098            else:
4099                c = (0.6, 0.6, 0.6)
4100
4101        tpoly = self._get_text3d_poly(
4102            txt, s, font, hspacing, vspacing, depth, italic, justify, literal
4103        )
4104
4105        super().__init__(tpoly, c, alpha)
4106
4107        self.pos(pos)
4108        self.lighting("off")
4109
4110        self.actor.PickableOff()
4111        self.actor.DragableOff()
4112        self.init_scale = s
4113        self.name = "Text3D"
4114        self.txt = txt
4115        self.justify = justify
4116
4117    def text(
4118        self,
4119        txt=None,
4120        s=1,
4121        font="",
4122        hspacing=1.15,
4123        vspacing=2.15,
4124        depth=0,
4125        italic=False,
4126        justify="",
4127        literal=False,
4128    ) -> "Text3D":
4129        """
4130        Update the text and some of its properties.
4131
4132        Check [available fonts here](https://vedo.embl.es/fonts).
4133        """
4134        if txt is None:
4135            return self.txt
4136        if not justify:
4137            justify = self.justify
4138
4139        poly = self._get_text3d_poly(
4140            txt, self.init_scale * s, font, hspacing, vspacing,
4141            depth, italic, justify, literal
4142        )
4143
4144        # apply the current transformation to the new polydata
4145        tf = vtki.new("TransformPolyDataFilter")
4146        tf.SetInputData(poly)
4147        tf.SetTransform(self.transform.T)
4148        tf.Update()
4149        tpoly = tf.GetOutput()
4150
4151        self._update(tpoly)
4152        self.txt = txt
4153        return self
4154
4155    def _get_text3d_poly(
4156        self,
4157        txt,
4158        s=1,
4159        font="",
4160        hspacing=1.15,
4161        vspacing=2.15,
4162        depth=0,
4163        italic=False,
4164        justify="bottom-left",
4165        literal=False,
4166    ) -> vtki.vtkPolyData:
4167        if not font:
4168            font = settings.default_font
4169
4170        txt = str(txt)
4171
4172        if font == "VTK":  #######################################
4173            vtt = vtki.new("VectorText")
4174            vtt.SetText(txt)
4175            vtt.Update()
4176            tpoly = vtt.GetOutput()
4177
4178        else:  ###################################################
4179
4180            stxt = set(txt)  # check here if null or only spaces
4181            if not txt or (len(stxt) == 1 and " " in stxt):
4182                return vtki.vtkPolyData()
4183
4184            if italic is True:
4185                italic = 1
4186
4187            if isinstance(font, int):
4188                lfonts = list(settings.font_parameters.keys())
4189                font = font % len(lfonts)
4190                font = lfonts[font]
4191
4192            if font not in settings.font_parameters.keys():
4193                fpars = settings.font_parameters["Normografo"]
4194            else:
4195                fpars = settings.font_parameters[font]
4196
4197            # ad hoc adjustments
4198            mono = fpars["mono"]
4199            lspacing = fpars["lspacing"]
4200            hspacing *= fpars["hspacing"]
4201            fscale = fpars["fscale"]
4202            dotsep = fpars["dotsep"]
4203
4204            # replacements
4205            if ":" in txt:
4206                for r in _reps:
4207                    txt = txt.replace(r[0], r[1])
4208
4209            if not literal:
4210                reps2 = [
4211                    (r"\_", "┭"),  # trick to protect ~ _ and ^ chars
4212                    (r"\^", "┮"),  #
4213                    (r"\~", "┯"),  #
4214                    ("**", "^"),  # order matters
4215                    ("e+0", dotsep + "10^"),
4216                    ("e-0", dotsep + "10^-"),
4217                    ("E+0", dotsep + "10^"),
4218                    ("E-0", dotsep + "10^-"),
4219                    ("e+", dotsep + "10^"),
4220                    ("e-", dotsep + "10^-"),
4221                    ("E+", dotsep + "10^"),
4222                    ("E-", dotsep + "10^-"),
4223                ]
4224                for r in reps2:
4225                    txt = txt.replace(r[0], r[1])
4226
4227            xmax, ymax, yshift, scale = 0.0, 0.0, 0.0, 1.0
4228            save_xmax = 0.0
4229
4230            notfounds = set()
4231            polyletters = []
4232            ntxt = len(txt)
4233            for i, t in enumerate(txt):
4234                ##########
4235                if t == "┭":
4236                    t = "_"
4237                elif t == "┮":
4238                    t = "^"
4239                elif t == "┯":
4240                    t = "~"
4241                elif t == "^" and not literal:
4242                    if yshift < 0:
4243                        xmax = save_xmax
4244                    yshift = 0.9 * fscale
4245                    scale = 0.5
4246                    continue
4247                elif t == "_" and not literal:
4248                    if yshift > 0:
4249                        xmax = save_xmax
4250                    yshift = -0.3 * fscale
4251                    scale = 0.5
4252                    continue
4253                elif (t in (" ", "\\n")) and yshift:
4254                    yshift = 0.0
4255                    scale = 1.0
4256                    save_xmax = xmax
4257                    if t == " ":
4258                        continue
4259                elif t == "~":
4260                    if i < ntxt - 1 and txt[i + 1] == "_":
4261                        continue
4262                    xmax += hspacing * scale * fscale / 4
4263                    continue
4264
4265                ############
4266                if t == " ":
4267                    xmax += hspacing * scale * fscale
4268
4269                elif t == "\n":
4270                    xmax = 0.0
4271                    save_xmax = 0.0
4272                    ymax -= vspacing
4273
4274                else:
4275                    poly = _get_font_letter(font, t)
4276                    if not poly:
4277                        notfounds.add(t)
4278                        xmax += hspacing * scale * fscale
4279                        continue
4280                    
4281                    if poly.GetNumberOfPoints() == 0:
4282                        continue
4283
4284                    tr = vtki.vtkTransform()
4285                    tr.Translate(xmax, ymax + yshift, 0)
4286                    pscale = scale * fscale / 1000
4287                    tr.Scale(pscale, pscale, pscale)
4288                    if italic:
4289                        tr.Concatenate([1, italic * 0.15, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1])
4290                    tf = vtki.new("TransformPolyDataFilter")
4291                    tf.SetInputData(poly)
4292