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