|
12 | 12 | "meshabox",
|
13 | 13 | "meshacylinder",
|
14 | 14 | "meshcylinders",
|
| 15 | + "meshcylinders", |
15 | 16 | "meshanellip",
|
16 | 17 | "meshunitsphere",
|
17 | 18 | "meshasphere",
|
|
29 | 30 | from iso2mesh.core import surf2mesh, vol2restrictedtri
|
30 | 31 | from iso2mesh.trait import meshreorient, volface, surfedge
|
31 | 32 | from iso2mesh.utils import *
|
32 |
| -from iso2mesh.modify import removeisolatednode |
| 33 | +from iso2mesh.modify import removeisolatednode, meshcheckrepair |
33 | 34 |
|
34 | 35 | # _________________________________________________________________________________________________________
|
35 | 36 |
|
@@ -309,6 +310,26 @@ def meshgrid5(*args):
|
309 | 310 |
|
310 | 311 |
|
311 | 312 | def meshgrid6(*args):
|
| 313 | + """ |
| 314 | + Generate a tetrahedral mesh from an N-D rectangular lattice by splitting |
| 315 | + each hypercube into 6 tetrahedra. |
| 316 | +
|
| 317 | + Parameters: |
| 318 | + v1, v2, v3, ... : array-like |
| 319 | + Numeric vectors defining the lattice in each dimension. |
| 320 | + Each vector must be of length >= 1. |
| 321 | +
|
| 322 | + Returns: |
| 323 | + node : ndarray |
| 324 | + Coordinates of the nodes in the factorial lattice created from (v1, v2, v3, ...). |
| 325 | + Each row corresponds to a node. |
| 326 | + elem : ndarray |
| 327 | + Integer array defining the simplices (tetrahedra) as indices into rows of `node`. |
| 328 | +
|
| 329 | + Notes: |
| 330 | + This function is part of the iso2mesh toolbox (http://iso2mesh.sf.net) |
| 331 | + Originally authored by John D'Errico, with modifications by Qianqian Fang. |
| 332 | + """ |
312 | 333 | # dimension of the lattice
|
313 | 334 | n = len(args)
|
314 | 335 |
|
@@ -456,72 +477,193 @@ def latticegrid(*args):
|
456 | 477 | # _________________________________________________________________________________________________________
|
457 | 478 |
|
458 | 479 |
|
459 |
| -def extrudecurve(c0, c1, curve, ndiv): |
460 |
| - if len(c0) != len(c1) or len(c0) != 3: |
461 |
| - raise ValueError("c0 and c1 must be 3D points of the same dimension!") |
462 |
| - |
463 |
| - if ndiv < 1: |
464 |
| - raise ValueError("ndiv must be at least 1!") |
465 |
| - |
466 |
| - curve = np.array(curve) |
467 |
| - if curve.shape[1] != 3: |
468 |
| - raise ValueError("curve must be a Nx3 array!") |
469 |
| - |
470 |
| - ncurve = curve.shape[0] |
471 |
| - nodes = np.zeros((ndiv * ncurve, 3)) |
472 |
| - for i in range(ndiv): |
473 |
| - alpha = i / (ndiv - 1) # linear interpolation factor |
474 |
| - point = (1 - alpha) * c0 + alpha * c1 |
475 |
| - nodes[i * ncurve : (i + 1) * ncurve, :] = curve + point |
476 |
| - |
477 |
| - elem = np.zeros((ncurve * (ndiv - 1) * 2, 4), dtype=int) |
478 |
| - for i in range(ndiv - 1): |
479 |
| - for j in range(ncurve): |
480 |
| - if j < ncurve - 1: |
481 |
| - elem[i * ncurve * 2 + j * 2, :] = [ |
482 |
| - i * ncurve + j, |
483 |
| - (i + 1) * ncurve + j, |
484 |
| - (i + 1) * ncurve + (j + 1), |
485 |
| - i * ncurve + (j + 1), |
486 |
| - ] |
487 |
| - elem[i * ncurve * 2 + j * 2 + 1, :] = [ |
488 |
| - (i + 1) * ncurve + j, |
489 |
| - (i + 1) * ncurve + (j + 1), |
490 |
| - i * ncurve + (j + 1), |
491 |
| - i * ncurve + j, |
492 |
| - ] |
493 |
| - |
494 |
| - return nodes, elem |
| 480 | +def extrudecurve( |
| 481 | + xy, yz, Nx=30, Nz=30, Nextrap=0, spacing=1, anchor=None, dotopbottom=0 |
| 482 | +): |
| 483 | + """ |
| 484 | + Create a triangular surface mesh by swinging a 2D spline along another 2D spline curve. |
| 485 | +
|
| 486 | + Parameters: |
| 487 | + xy : ndarray |
| 488 | + A 2D spline path, along which the surface is extruded, defined on the x-y plane. |
| 489 | + yz : ndarray |
| 490 | + A 2D spline which will move along the path to form a surface, defined on the y-z plane. |
| 491 | + Nx : int, optional |
| 492 | + The count of sample points along the extrusion path (xy), default is 30. |
| 493 | + Nz : int, optional |
| 494 | + The count of sample points along the curve to be extruded (yz), default is 30. |
| 495 | + Nextrap : int, optional |
| 496 | + Number of points to extrapolate outside of the xy/yz curves, default is 0. |
| 497 | + spacing : float, optional |
| 498 | + Define a spacing scaling factor for spline interpolations, default is 1. |
| 499 | + anchor : list or ndarray, optional |
| 500 | + The 3D point in the extruded curve plane (yz) that is aligned at the nodes long the extrusion path. |
| 501 | + If not provided, it is set as the point on the interpolated yz with the largest y-value. |
| 502 | + dotopbottom : int, optional |
| 503 | + If set to 1, tessellated top and bottom faces will be added, default is 0. |
| 504 | +
|
| 505 | + Returns: |
| 506 | + node : ndarray |
| 507 | + 3D node coordinates for the generated surface mesh. |
| 508 | + face : ndarray |
| 509 | + Triangular face patches of the generated surface mesh, each row represents a triangle. |
| 510 | + yz0 : ndarray |
| 511 | + Sliced yz curve at the start. |
| 512 | + yz1 : ndarray |
| 513 | + Sliced yz curve at the end. |
| 514 | +
|
| 515 | + -- this function is part of iso2mesh toolbox (http://iso2mesh.sf.net) |
| 516 | + """ |
| 517 | + from scipy.interpolate import splev, splrep |
| 518 | + |
| 519 | + # Compute interpolation points along the xy curve |
| 520 | + xrange = np.max(xy[:, 0]) - np.min(xy[:, 0]) |
| 521 | + dx = xrange / Nx |
| 522 | + xi = np.arange( |
| 523 | + np.min(xy[:, 0]) - Nextrap * dx, |
| 524 | + np.max(xy[:, 0]) + Nextrap * dx + spacing * dx / 2, |
| 525 | + spacing * dx, |
| 526 | + ) |
| 527 | + pxy = splrep(xy[:, 0], xy[:, 1]) |
| 528 | + |
| 529 | + # Evaluate the interpolated y values and gradients |
| 530 | + yi = splev(xi, pxy) |
| 531 | + dy = np.gradient(yi) |
| 532 | + dxi = np.gradient(xi) |
| 533 | + |
| 534 | + nn = np.sqrt(dxi**2 + dy**2) |
| 535 | + normaldir = np.vstack((dxi / nn, dy / nn)).T |
| 536 | + |
| 537 | + # Compute interpolation points along the yz curve |
| 538 | + zrange = np.max(yz[:, 1]) - np.min(yz[:, 1]) |
| 539 | + dz = zrange / Nz |
| 540 | + zi = np.arange( |
| 541 | + np.min(yz[:, 1]) - Nextrap * dz, |
| 542 | + np.max(yz[:, 1]) + Nextrap * dz + spacing * dz / 2, |
| 543 | + spacing * dz, |
| 544 | + ) |
| 545 | + pyz = splrep(yz[:, 1], yz[:, 0]) |
| 546 | + |
| 547 | + yyi = splev(zi, pyz) |
| 548 | + |
| 549 | + # Determine anchor point if not provided |
| 550 | + if anchor is None: |
| 551 | + loc = np.argmax(yyi) |
| 552 | + anchor = [0, yyi[loc], zi[loc]] |
| 553 | + |
| 554 | + # Initialize node and face arrays |
| 555 | + node = np.zeros((len(zi) * len(xi), 3)) |
| 556 | + face = np.zeros((2 * (len(zi) - 1) * (len(xi) - 1), 3), dtype=int) |
| 557 | + |
| 558 | + # Generate the base yz profile points |
| 559 | + xyz = np.column_stack((np.zeros_like(yyi), yyi, zi)) |
| 560 | + for i in range(len(xi)): |
| 561 | + # Compute local rotation matrix |
| 562 | + rot2d = np.array( |
| 563 | + [[normaldir[i, 0], -normaldir[i, 1]], [normaldir[i, 1], normaldir[i, 0]]] |
| 564 | + ) |
| 565 | + offset = [xi[i], yi[i], anchor[2]] |
| 566 | + newyz = xyz.copy() |
| 567 | + newyz[:, :2] = (rot2d @ (newyz[:, :2] - anchor[:2]).T).T + offset[:2] |
| 568 | + node[i * len(zi) : (i + 1) * len(zi), :] = newyz |
| 569 | + |
| 570 | + # Create faces between segments |
| 571 | + if i > 0: |
| 572 | + a = np.arange(len(zi) - 1) |
| 573 | + b = a + 1 |
| 574 | + f1 = np.stack( |
| 575 | + (a + (i - 1) * len(zi), a + i * len(zi), b + (i - 1) * len(zi)), axis=-1 |
| 576 | + ) |
| 577 | + f2 = np.stack( |
| 578 | + (b + (i - 1) * len(zi), a + i * len(zi), b + i * len(zi)), axis=-1 |
| 579 | + ) |
| 580 | + face[(i - 1) * 2 * (len(zi) - 1) : (i) * 2 * (len(zi) - 1)] = np.vstack( |
| 581 | + (f1, f2) |
| 582 | + ) |
| 583 | + |
| 584 | + # Save yz slices for later output |
| 585 | + if i == Nextrap: |
| 586 | + yz0 = newyz[Nextrap : len(zi) - Nextrap, :] |
| 587 | + if i == len(xi) - Nextrap - 1: |
| 588 | + yz1 = newyz[Nextrap : len(zi) - Nextrap, :] |
| 589 | + |
| 590 | + # Add two flat polygons on the top and bottom of the contours |
| 591 | + # to ensure the enclosed surface is not truncated by meshfix |
| 592 | + if dotopbottom == 1: |
| 593 | + from scipy.spatial import Delaunay |
| 594 | + |
| 595 | + C = np.vstack((np.arange(0, len(xi) - 1), np.arange(1, len(xi)))).T |
| 596 | + C = np.vstack((C, [[len(xi) - 1, 0]])) |
| 597 | + dt = Delaunay(np.column_stack((xi, yi))) |
| 598 | + io = dt.find_simplex(np.column_stack((xi, yi))) >= 0 |
| 599 | + endface = dt.simplices[io] |
| 600 | + endface = (endface - 1) * len(zi) + 1 |
| 601 | + face = np.vstack((face, endface, endface + len(zi) - 1)) |
| 602 | + |
| 603 | + # Check and repair mesh geometry |
| 604 | + node, face = meshcheckrepair(node, face, "deep") |
| 605 | + |
| 606 | + return node, face, yz0, yz1 |
495 | 607 |
|
496 | 608 |
|
497 | 609 | # _________________________________________________________________________________________________________
|
498 | 610 |
|
499 | 611 |
|
500 |
| -def meshcylinders(c0, c1, r, tsize=0, maxvol=0, ndiv=20): |
501 |
| - if np.any(np.array(r) <= 0): |
502 |
| - raise ValueError("Radius must be greater than zero.") |
| 612 | +def meshcylinders(c0, v, seglen, r, tsize=None, maxvol=None, ndiv=20): |
| 613 | + """ |
| 614 | + create the surface and (optionally) tetrahedral mesh of multiple segments of 3D cylinders |
| 615 | +
|
| 616 | + author: Qianqian Fang, <q.fang at neu.edu> |
503 | 617 |
|
504 |
| - if np.array(c0).shape != (3,) or np.array(c1).shape != (3,): |
505 |
| - raise ValueError("c0 and c1 must be 3D points.") |
| 618 | + Parameters: |
| 619 | + c0: cylinder list axis's starting point |
| 620 | + v: directional vector of the cylinder |
| 621 | + seglen: a scalar or a vector denoting the length of each |
| 622 | + cylinder segment along the direction of v |
| 623 | + args: tsize, maxvol, ndiv - see meshacylinder for details |
506 | 624 |
|
507 |
| - if len(r) == 1: |
508 |
| - r = [r[0], r[0]] |
| 625 | + Returns: |
| 626 | + node, face, elem - see meshacylinder for details |
509 | 627 |
|
510 |
| - r = np.array(r).flatten() |
| 628 | + -- this function is part of iso2mesh toolbox (http://iso2mesh.sf.net) |
| 629 | + """ |
| 630 | + seglen = np.cumsum(seglen) |
| 631 | + c0 = np.array(c0) |
| 632 | + v = np.array(v) |
| 633 | + ncyl, fcyl = meshacylinder(c0, c0 + v * seglen[0], r, 0, 0, ndiv) |
| 634 | + |
| 635 | + if len(seglen) == 1: |
| 636 | + node = ncyl |
| 637 | + face = fcyl |
| 638 | + return node, face |
| 639 | + |
| 640 | + for i in range(1, len(seglen)): |
| 641 | + ncyl1, fcyl1 = meshacylinder( |
| 642 | + c0 + v * seglen[i - 1], c0 + v * seglen[i], r, 0, 0, ndiv |
| 643 | + ) |
| 644 | + fcyl1 = [[(np.array(f[0]) + ncyl.shape[0]).tolist(), f[1]] for f in fcyl1] |
| 645 | + fcyl1 = fcyl1[:-2] + [fcyl1[-1]] |
| 646 | + fcyl.extend(fcyl1) |
| 647 | + ncyl = np.vstack((ncyl, ncyl1)) |
511 | 648 |
|
512 |
| - if len(r) == 2: |
513 |
| - r = np.array([r[0], r[0], r[1]]) |
| 649 | + ncyl, I, J = np.unique( |
| 650 | + np.round(ncyl, 10), axis=0, return_index=True, return_inverse=True |
| 651 | + ) |
514 | 652 |
|
515 |
| - len_axis = np.linalg.norm(np.array(c1) - np.array(c0)) |
| 653 | + fcyl = [[(J[np.array(f[0]) - 1] + 1).tolist(), f[1]] for f in fcyl] |
516 | 654 |
|
517 |
| - if tsize == 0: |
518 |
| - tsize = min(r) * 0.1 |
| 655 | + if tsize == 0 and maxvol == 0: |
| 656 | + return ncyl, fcyl |
519 | 657 |
|
520 |
| - if maxvol == 0: |
521 |
| - maxvol = tsize**3 * 0.2 |
| 658 | + if not tsize: |
| 659 | + tsize = seglen[-1] * 0.1 |
| 660 | + if not maxvol: |
| 661 | + maxvol = tsize * tsize * tsize |
522 | 662 |
|
523 |
| - node, face, elem = meshacylinder(c0, c1, r, tsize, maxvol, ndiv) |
| 663 | + centroid = np.cumsum(np.concatenate(([0], seglen[:-1]))) + seglen[-1] * 0.5 |
| 664 | + seeds = c0 + v * centroid[:, None] |
524 | 665 |
|
| 666 | + node, elem, face = surf2mesh(ncyl, fcyl, None, None, 1, maxvol, seeds, None, 0) |
525 | 667 | return node, face, elem
|
526 | 668 |
|
527 | 669 |
|
|
0 commit comments