mesh
triwarp.mesh
A cached, composition-based triangle mesh container (mirrors trimesh.Trimesh).
Trimesh
Trimesh(
vertices: array[vec3],
faces: array[int32],
*,
initial_cache: dict[str, object] | None = None,
)
A triangle mesh with lazily cached derived quantities.
Holds a vertices/faces pair (the same flat-int32 convention used throughout
triwarp) and computes derived geometry, topology, and validity predicates on first
access, caching the result. Trimesh is frozen: there are no setters, so a cached value
can never silently go stale from a Python-level mutation. Composition rather than
inheritance is used for the warp.Mesh BVH — building it eagerly on every mesh would
waste GPU memory on meshes that never issue a ray or proximity query, so it is built lazily
behind warp_mesh on first use instead.
Every cached property returns the same object on each access — an array, a tuple of arrays or
a sparse matrix, aliased across repeated accesses and (for warp_mesh) with the mesh's own
vertices/faces buffers — so callers must not mutate a returned buffer in place. That
matters most where they are passed into the free functions as precomputed arguments: a
wrapper that rewrites such a buffer copies it first
(filter_normals is the one that does). If a buffer is
deliberately mutated in place by a kernel, call
invalidate afterward to drop every cached value
(including the BVH); otherwise use
with_vertices /
with_faces, which return a new Trimesh and carry
forward whichever cached values are still valid.
Scalar-valued properties (area, centroid, bounds, enclosing_diagonal,
mean_edge_length, euler_characteristic, and every is_* predicate) synchronize the result
from device to host on first access; the synchronized Python value is then cached like any
other property.
Most of these are also what the free functions accept as an optional precomputed argument, so
the cache is worth more than the repeat accesses on this class: pass edges_sorted,
face_adjacency, halfedge_twins, vertex_one_rings, vertex_face_adjacency,
face_normals / face_areas, cotmatrix_entries, bounds, laplacian_operator or an
operator bundle into the wrapper that takes it and the whole assembly is skipped. The discrete
operators at the bottom of the class (cotmatrix through vector_heat_operators) are the
heaviest of these and the reason a solver run over one mesh should go through a Trimesh.
Caching pays off most when the same mesh is reused across many calls, and least where a single solve dominates the cost of any one call.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
array[vec3]
|
|
required |
faces
|
array[int32]
|
Triangle indices as |
required |
initial_cache
|
dict[str, object] | None
|
Optional pre-populated cache entries, keyed by property name. Used by
|
None
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If a rank-2 |
Examples:
mesh = tw.Trimesh.from_warp_mesh(warp_mesh)
mesh.face_normals # computed once, cached
mesh.face_areas # already cached as a by-product of face_normals
hits = tw.ray.intersects_any(mesh.warp_mesh, origins, directions)
See Also
Construct a Trimesh from a vertex buffer and a face index buffer.
Source code in triwarp/mesh.py
347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 | |
__slots__
class-attribute
instance-attribute
__slots__ = ('_cache', '_faces', '_vertices')
vertices
property
vertices: array[vec3]
(n_vertices,) mesh vertex positions.
faces
property
faces: array[int32]
Length-3 * n_faces flat wp.int32 triangle index buffer.
n_vertices
property
n_vertices: int
Number of vertices in the vertices buffer (its length, not the referenced count).
n_faces
property
n_faces: int
Number of triangles (faces.size // 3).
device
property
device: Device
Warp device holding vertices and faces.
from_warp_mesh
classmethod
from_warp_mesh(mesh: Mesh) -> Trimesh
Wrap an existing warp.Mesh, seeding warp_mesh so its BVH is never rebuilt.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mesh
|
Mesh
|
Source mesh; |
required |
Returns:
| Type | Description |
|---|---|
Trimesh
|
New instance whose |
Source code in triwarp/mesh.py
368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 | |
warp_mesh
warp_mesh() -> wp.Mesh
Build the vertices / faces pair into a warp.Mesh, constructing its BVH.
Raises:
| Type | Description |
|---|---|
ValueError
|
If the mesh has zero faces — building a |
See Also
Source code in triwarp/mesh.py
411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 | |
face_normals
face_normals() -> wp.array[wp.vec3]
Length-n_faces unit face normals (zero for degenerate faces).
Computed together with face_areas; whichever is
accessed first also caches the other.
Source code in triwarp/mesh.py
430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 | |
face_areas
face_areas() -> wp.array[wp.float32]
Length-n_faces triangle areas.
Computed together with face_normals; whichever
is accessed first also caches the other.
Source code in triwarp/mesh.py
447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 | |
area
area() -> float
Total surface area (sum of face_areas).
Notes
Triggers a device-to-host synchronization on first access.
See Also
Source code in triwarp/mesh.py
468 469 470 471 472 473 474 475 476 477 478 479 480 481 | |
face_angles
face_angles() -> twt.Array2dFloat32
Shape (n_faces, 3) interior angles in radians, aligned with each face's corners.
Source code in triwarp/mesh.py
483 484 485 486 487 488 489 490 491 492 493 | |
triangles_center
triangles_center() -> wp.array[wp.vec3]
Length-n_faces barycentre of each triangle (the mean of its three corners).
Notes
Named for trimesh.Trimesh.triangles_center rather than for the function it calls;
matching trimesh's property names is this facade's job, and the module-level function is
face_centroids.
See Also
triwarp.triangles.face_centroids
centroid
The area-weighted centre of the whole surface, which is not the mean of these.
Source code in triwarp/mesh.py
495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 | |
centroid
centroid() -> wp.vec3
Area-weighted centroid of the mesh surface (all-NaN for an empty mesh).
Notes
Triggers a device-to-host synchronization on first access.
Source code in triwarp/mesh.py
514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 | |
volume
volume() -> float
Signed volume enclosed by the surface (meaningful only for a closed, consistent mesh).
Notes
Triggers a device-to-host synchronization on first access. Accumulated in float32,
unlike the volume triwarp.measures.moments computes alongside
center_mass -- the two agree to that precision and
are not the same number, which is why this property does not fill that call's cache.
See Also
triwarp.measures.volume
is_volume
The check for the precondition this quantity needs.
trimesh.Trimesh.volume
Source code in triwarp/mesh.py
530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 | |
center_mass
center_mass() -> wp.vec3
Centre of mass of the enclosed solid at unit density (all-NaN for an empty mesh).
Not centroid, which is the area-weighted centre of
the surface: the two differ on any solid whose mass is not distributed like its shell.
Computed together with moment_inertia; whichever
is accessed first also caches the other.
Notes
Triggers a device-to-host synchronization on first access (three, in fact -- every return
of triwarp.measures.moments is a host value).
Source code in triwarp/mesh.py
551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 | |
moment_inertia
moment_inertia() -> wp.mat33d
(3, 3) inertia tensor about the centre of mass, at unit density.
float64: the second moments scale as length ** 5, so a float32 accumulation
loses their low digits on any sizeable mesh.
Computed together with center_mass; whichever is
accessed first also caches the other.
Notes
Triggers a device-to-host synchronization on first access. Being referred to the centre of
mass rather than to the origin, it is unchanged by a translation --
transform carries it through one.
Source code in triwarp/mesh.py
577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 | |
bounds
bounds() -> tuple[wp.vec3, wp.vec3]
Axis-aligned bounding box of vertices, as (min_bound, max_bound).
Pass it to any wrapper taking a bounds= argument --
marching_cubes,
signed_distance_grid,
query_nearest,
grid_points -- so the box is reduced once per mesh rather
than once per call.
Notes
Triggers a device-to-host synchronization on first access. (+inf, -inf) for an empty
mesh, the aabb convention.
See Also
Source code in triwarp/mesh.py
604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 | |
extents
extents() -> wp.vec3
Side lengths of the axis-aligned bounds box.
Notes
Costs no device work of its own: it subtracts the cached box on the host. (-inf, ...)
for an empty mesh, following bounds' (+inf, -inf)
convention.
Source code in triwarp/mesh.py
628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 | |
enclosing_diagonal
enclosing_diagonal() -> float
Diagonal length of bounds: this package's default mesh-query search radius.
Notes
Costs no device work of its own, unlike
enclosing_diagonal, which reduces the box again --
it reads the cached bounds on the host. inf for an empty mesh, that function's
convention.
A query wrapper's own default is the diagonal of the box around the mesh and the query points, which this is not; it is the right radius only when the queries lie inside the mesh's own box.
See Also
triwarp.bounds.enclosing_diagonal
bounds
trimesh.Trimesh.scale
The same quantity under trimesh's name for it (no Sphinx inventory entry to link).
Source code in triwarp/mesh.py
648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 | |
vertex_normals
vertex_normals() -> wp.array[wp.vec3]
Length-n_vertices angle-weighted unit vertex normals.
Matches trimesh's default vertex-normal weighting (interior-angle weighted).
See Also
vertices.vertex_normals at weighting="angle"
trimesh.Trimesh.vertex_normals
Source code in triwarp/mesh.py
674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 | |
vertex_defects
vertex_defects() -> wp.array[wp.float32]
Length-n_vertices discrete angle defect (2π minus incident corner angles).
See Also
Source code in triwarp/mesh.py
694 695 696 697 698 699 700 701 702 703 | |
nondegenerate_faces
nondegenerate_faces() -> wp.array[wp.bool]
Length-n_faces mask; True where the triangle has non-zero area.
Named for trimesh.Trimesh.nondegenerate_faces rather than for the function it calls:
matching trimesh's property names is this facade's whole job, so the property keeps the
spelling a reader arrives with while the module-level function is
face_nondegenerate_mask.
See Also
Source code in triwarp/mesh.py
705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 | |
mean_edge_length
mean_edge_length() -> float
Mean length over all 3 * n_faces per-face edges (0.0 for an empty mesh).
Notes
Triggers a device-to-host synchronization on first access.
See Also
Source code in triwarp/mesh.py
721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 | |
edges
edges() -> twt.Array2dInt32
Shape (n_faces * 3, 2) directed triangle edges, in face-corner order.
Source code in triwarp/mesh.py
739 740 741 742 743 744 745 746 747 748 749 | |
edges_sorted
edges_sorted() -> twt.Array2dInt32
Shape (n_faces * 3, 2) edges from edges, each row sorted (smaller index first).
Source code in triwarp/mesh.py
751 752 753 754 755 756 757 758 759 760 761 | |
edges_face
edges_face() -> wp.array[wp.int32]
Length-n_faces * 3 face index for each row of edges / edges_sorted.
Source code in triwarp/mesh.py
763 764 765 766 767 768 769 770 771 772 773 | |
edges_unique
edges_unique() -> twt.Array2dInt32
Shape (m, 2) unique undirected edges, m <= n_faces * 3.
Also caches edges_unique_inverse as a
by-product.
Source code in triwarp/mesh.py
775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 | |
edges_unique_inverse
edges_unique_inverse() -> wp.array[wp.int32]
Length-n_faces * 3 inverse mapping into edges_unique (reconstructs edges_sorted).
Source code in triwarp/mesh.py
794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 | |
faces_unique_edges
faces_unique_edges() -> twt.Array2dInt32
Shape (n_faces, 3) index into edges_unique.
Row f holds the unique-edge slot of each of face f's three edges, in face-corner
order, so edges_unique[faces_unique_edges[f, k]] is that corner's edge.
Notes
Costs no device work: it is
edges_unique_inverse viewed as (n, 3),
and shares that buffer rather than copying it.
Source code in triwarp/mesh.py
812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 | |
edges_unique_length
edges_unique_length() -> wp.array[wp.float32]
Length-m Euclidean length of each edges_unique row.
Source code in triwarp/mesh.py
833 834 835 836 837 838 839 840 841 842 843 844 845 | |
face_adjacency
face_adjacency() -> twt.Array2dInt32
Shape (m, 2) face index pairs that share an undirected edge.
Also caches face_adjacency_edges as a
by-product.
Source code in triwarp/mesh.py
847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 | |
face_adjacency_edges
face_adjacency_edges() -> twt.Array2dInt32
Shape (m, 2) shared vertex pair for each face_adjacency row.
See Also
Source code in triwarp/mesh.py
871 872 873 874 875 876 877 878 879 880 881 882 883 884 | |
face_adjacency_unshared
face_adjacency_unshared() -> twt.Array2dInt32
Shape (m, 2) vertex on each adjacent face not on their shared edge.
Source code in triwarp/mesh.py
886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 | |
face_adjacency_angles
face_adjacency_angles() -> wp.array[wp.float32]
Length-m unsigned angle in radians between each face_adjacency pair.
Source code in triwarp/mesh.py
902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 | |
face_adjacency_projections
face_adjacency_projections() -> wp.array[wp.float32]
Length-m projection of each adjacent pair's unshared vertices onto the other's plane.
Negative where the pair is convex, which is the sign
face_adjacency_convex thresholds.
See Also
triwarp.adjacency.face_adjacency_projections
face_adjacency_convex
trimesh.Trimesh.face_adjacency_projections
Source code in triwarp/mesh.py
919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 | |
face_adjacency_convex
face_adjacency_convex() -> wp.array[wp.bool]
Length-m mask; True where an adjacent face pair meets convexly.
See Also
triwarp.adjacency.face_adjacency_convex
face_adjacency_projections
trimesh.Trimesh.face_adjacency_convex
Source code in triwarp/mesh.py
942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 | |
face_connected_component_labels
face_connected_component_labels() -> wp.array[wp.int32]
Length-n_faces connected-component label per face (face-adjacency graph).
Notes
Calls
connected_component_labels_from_edges
directly rather than going through
face_connected_component_labels, so
that the cached face_adjacency is reused instead of rebuilt. Same labelling engine, same
answer.
Source code in triwarp/mesh.py
962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 | |
body_count
body_count() -> int
Component count of the face-adjacency graph (0 for an empty mesh).
Notes
Triggers a device-to-host synchronization on first access. Counts components of the
face graph, so an isolated vertex referenced by no face is not a body -- unlike
igl.connected_components over the vertex adjacency, which counts each one.
See Also
face_connected_component_labels
triwarp.combine.split
Materializes the bodies this counts.
trimesh.Trimesh.body_count
Source code in triwarp/mesh.py
984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 | |
vertex_face_adjacency
vertex_face_adjacency() -> tuple[
wp.array[wp.int32], wp.array[wp.int32]
]
Incidence CSR of the faces touching each vertex, as (vertex_faces, offsets).
Each row is a set: use vertex_one_rings where the rotational order around the vertex
is what matters, at the price of needing an edge-manifold mesh.
Source code in triwarp/mesh.py
1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 | |
halfedge_twins
halfedge_twins() -> wp.array[wp.int32]
Length-3 * n_faces opposite halfedge of every halfedge, or -1 on a boundary.
Halfedge h = 3 * f + k runs from faces[3f + k] to faces[3f + (k + 1) % 3], so
next and prev are index arithmetic and this array is all a walk needs to cross an
edge.
Raises:
| Type | Description |
|---|---|
ValueError
|
Propagated from |
Notes
Triggers a device-to-host synchronization on first access (that manifoldness check).
Source code in triwarp/mesh.py
1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 | |
vertex_one_rings
vertex_one_rings() -> tuple[
wp.array[wp.int32],
wp.array[wp.int32],
wp.array[wp.bool],
]
Counter-clockwise outgoing halfedges per vertex: (ring_halfedges, offsets, is_boundary).
Built from the cached halfedge_twins, so accessing either first pays for that array once.
Raises:
| Type | Description |
|---|---|
ValueError
|
Propagated from |
Notes
Triggers a device-to-host synchronization on first access (that manifoldness check).
Source code in triwarp/mesh.py
1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 | |
boundary_edges
boundary_edges() -> twt.Array2dInt32
Shape (n_boundary, 2) undirected boundary edges (each row sorted, min-first).
See Also
Source code in triwarp/mesh.py
1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 | |
oriented_boundary_edges
oriented_boundary_edges() -> twt.Array2dInt32
Shape (n_boundary, 2) directed boundary edges, preserving face winding.
Source code in triwarp/mesh.py
1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 | |
boundary_loops
boundary_loops() -> list[wp.array[wp.int32]]
One ordered vertex-index array per boundary loop (empty list if the mesh is closed).
See Also
Source code in triwarp/mesh.py
1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 | |
boundary_vertex_indices
boundary_vertex_indices() -> wp.array[wp.int32]
Sorted unique vertex indices lying on the mesh boundary.
Source code in triwarp/mesh.py
1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 | |
euler_characteristic
euler_characteristic() -> int
Euler characteristic V - E + F (0 for an empty mesh).
Notes
Triggers a device-to-host synchronization on first access. Equivalent to
euler_characteristic, recomposed here to
reuse the cached edges_unique count instead of recomputing it.
Source code in triwarp/mesh.py
1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 | |
is_edge_manifold
is_edge_manifold() -> bool
Whether every undirected edge is shared by one or two faces.
Notes
Triggers a device-to-host synchronization on first access.
See Also
Source code in triwarp/mesh.py
1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 | |
is_vertex_manifold
is_vertex_manifold() -> bool
Whether every referenced vertex has a single edge-connected fan of faces.
Notes
Triggers a device-to-host synchronization on first access.
Source code in triwarp/mesh.py
1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 | |
is_winding_consistent
is_winding_consistent() -> bool
Whether every shared edge is traversed in opposite directions by its two faces.
Notes
Triggers a device-to-host synchronization on first access. Equivalent to
is_winding_consistent, recomposed here to
reuse the cached edges / edges_sorted.
Source code in triwarp/mesh.py
1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 | |
is_orientable
is_orientable() -> bool
Whether the faces admit a consistent orientation (allowing per-face flips).
Notes
Triggers a device-to-host synchronization on first access. Unlike the other
adjacency-based predicates on this class, this does not reuse face_adjacency: the
underlying propagation engine rebuilds it internally and does not accept a
precomputed adjacency.
See Also
Source code in triwarp/mesh.py
1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 | |
is_watertight
is_watertight() -> bool
Whether the mesh bounds a closed volume with no self-intersections.
Follows Open3D's IsWatertight semantics (edge-manifold-closed, vertex-manifold, and
not self-intersecting) — not the same predicate as trimesh.Trimesh.is_watertight,
which only checks that every edge is shared by exactly two faces.
Notes
Triggers a device-to-host synchronization on first access. Passes the cached warp_mesh,
so the self-intersection broad phase reuses that BVH instead of building a second one --
the same reuse is_self_intersecting gets
for free by taking a wp.Mesh directly.
Source code in triwarp/mesh.py
1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 | |
is_self_intersecting
is_self_intersecting() -> bool
Whether any two non-adjacent triangles of the mesh intersect.
Raises:
| Type | Description |
|---|---|
ValueError
|
If the mesh has zero faces (see |
Notes
Triggers a device-to-host synchronization on first access. Reuses the cached
warp_mesh BVH for its broad phase.
Source code in triwarp/mesh.py
1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 | |
is_volume
is_volume() -> bool
Whether the mesh is a valid closed volume with outward-facing normals.
Watertight in the trimesh sense (every edge shared by exactly two faces), winding-consistent, and enclosing a positive signed volume.
Notes
Triggers a device-to-host synchronization on first access.
Source code in triwarp/mesh.py
1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 | |
cotmatrix_entries
cotmatrix_entries() -> twt.Array2dFloat32
Shape (n_faces, 3) per-triangle half-cotangent weights, in igl's edge order.
The float32 table, and that costs a float64 consumer nothing: the free function
computes these weights in float32 -- the vertex precision -- whatever dtype is asked
for, and casts on write, so a float64 request would return this same table widened.
Every consumer accepts either precision and casts to the matrix dtype in one build, so
pass this to cotmatrix,
connection_laplacian,
heat_operators or
crouzeix_raviart_cotmatrix at either
precision -- which is what this class's own float32 cotmatrix and float64
heat_operators both do.
Source code in triwarp/mesh.py
1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 | |
cotmatrix
cotmatrix() -> wps.BsrMatrix[wp.float32]
Cotangent stiffness matrix: the float32 discrete Laplace-Beltrami operator.
Assembled from the cached cotmatrix_entries. Diagonal entries are negative and each row
sums to zero, so -L is positive semi-definite on a closed mesh.
Notes
float32, which is the right precision for a product (an energy, a residual, a filter
weight) and not for a solve -- the heat solvers and
triwarp.parametrization assemble their own float64
operators, and heat_operators caches the one they share.
See Also
triwarp.laplacian.cotmatrix
cotmatrix_entries
mass_matrix_entries
triwarp.energies.k_harmonic
Takes this operator and mass_matrix_entries as its two arguments.
Source code in triwarp/mesh.py
1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 | |
mass_matrix_entries
mass_matrix_entries() -> wp.array[wp.float32]
Length-n_vertices barycentric lumped mass: a third of each incident triangle's area.
Built from the cached face_areas, so this is a scatter and nothing else.
Notes
float32. A float64 solve wants
mass_matrix_entries at that dtype, which
accumulates in the requested precision rather than casting this -- the two are not the same
array widened.
Source code in triwarp/mesh.py
1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 | |
laplacian_operator
laplacian_operator() -> wps.BsrMatrix[wp.float32]
Row-normalized 1-ring averaging operator (the uniform / umbrella Laplacian).
Exactly what every position filter in triwarp.smoothing builds for
itself when its laplacian_operator= argument is None, so passing this hoists the
assembly out of a multi-filter or multi-call pass.
Notes
The directed mesh.edges adjacency, the shared default.
filter_neighborhood_average is the one
filter that wants the symmetric adjacency instead
(laplacian at symmetric=True) and must not be handed
this one; the two differ only on a mesh with an open boundary.
Source code in triwarp/mesh.py
1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 | |
vertex_tangent_frames
vertex_tangent_frames() -> tuple[
wp.array[wp.vec3], wp.array[wp.vec3], wp.array[wp.vec3]
]
Orthonormal tangent frame at every vertex as (basis_x, basis_y, normal).
The gauge every 2-D tangent quantity on this mesh is measured in. Built from the cached
vertex_normals and vertex_one_rings, and its third element is vertex_normals.
Raises:
| Type | Description |
|---|---|
ValueError
|
Propagated from |
Source code in triwarp/mesh.py
1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 | |
heat_operators
heat_operators() -> tw.heat.HeatOperators
Source-independent operator bundle for the heat method, at the default diffusion time.
Pass it to heat_geodesic or
geodesic_path through their operators=
argument: everything in the bundle depends on the mesh alone, so distance from many
different source sets costs one assembly.
Notes
At t = None (the squared mean unique-edge length, igl::heat_geodesics' default) and
use_robust=False. A mesh with degenerate triangles, or a caller sweeping t, wants
heat_operators directly -- and can still pass this
class's cotmatrix_entries into it.
Source code in triwarp/mesh.py
1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 | |
vector_heat_operators
vector_heat_operators() -> tw.heat.VectorHeatOperators
Operator bundle for the vector heat method, at the default diffusion time.
(vector_system, scalar, frames, preconditioner): the 2 x 2-block connection system,
the scalar heat_operators, the vertex_tangent_frames, and the connection system's own
Jacobi preconditioner. Pass it to
transport_tangent_vectors,
log_map,
extend_scalar or
heat_signed_distance through their
operators= argument.
Its second and third fields are this class's own heat_operators and
vertex_tangent_frames, so the three properties share one assembly however they are
reached.
Notes
At t = None, the same default heat_operators uses -- which is load-bearing rather
than incidental: log_map's radius is asserted to be the
heat_geodesic distance, so the two systems must
share a diffusion time.
Source code in triwarp/mesh.py
1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 | |
contains
contains(points: array[vec3]) -> wp.array[wp.bool]
Test which query points lie inside the mesh, by ray parity against the cached BVH.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
points
|
array[vec3]
|
|
required |
Returns:
| Type | Description |
|---|---|
array[bool]
|
Length- |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the mesh has zero faces (see |
Notes
Only meaningful for a closed, consistently wound surface --
is_volume is the check for it. Reuses the cached
warp_mesh rather than building a BVH per call, which is the whole reason to reach for
this instead of the free function.
See Also
triwarp.ray.contains_points
triwarp.proximity.signed_distance_on_mesh
A signed distance rather than a bit, from the same BVH.
trimesh.Trimesh.contains
Source code in triwarp/mesh.py
1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 | |
sample
sample(
count: int, *, seed: int | None = None
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]
Sample points uniformly over the surface, area-weighted.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
count
|
int
|
Number of samples to draw. |
required |
seed
|
int | None
|
Seed for the sampler; non-reproducible when |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
points |
array[vec3]
|
|
face_indices |
array[int32]
|
Length- |
Notes
Passes the cached face_areas as the sampling weight,
so repeated draws from one mesh share that reduction. Returns the face indices as well as
the points, where trimesh.Trimesh.sample returns them only on request.
See Also
triwarp.sample.sample_surface
triwarp.sample.sample_surface_blue_noise
Sampling with a minimum separation rather than independently.
trimesh.Trimesh.sample
Source code in triwarp/mesh.py
1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 | |
transform
transform(
matrix: mat44 | array[mat44],
*,
assume: str | None = None,
) -> Trimesh
Return a new Trimesh under an affine transform, carrying forward whatever survives it.
Trimesh is frozen, so this returns a new instance rather than moving this one -- and
that is the fast path, not merely the safe one. How much of the cache survives is
decided by what the transform preserves
(classify_transform), and for a rigid motion
that is everything expensive: angles, areas, the cotangent table and the assembled
cotmatrix are all isometry invariants, so only the
directions and the bounding box are touched.
| transform | recomputed on the new mesh |
|---|---|
| translation | the BVH, and nothing else |
| rigid, reflection | the BVH and bounds; directions are rotated, not rebuilt |
| similarity | the above, plus the length and area quantities |
| affine | everything but connectivity |
Face winding is reversed when matrix mirrors, which keeps normals outward and costs
the orientation-dependent caches on top of the row above --
vertex_one_rings, the directed edge tables and
the per-corner tables among them.
Carrying the cache forward is cheaper than transforming the buffers and rebuilding a
Trimesh around them whenever the mesh is reused for further queries: the saving is a
count of assembly launches skipped, so it is worth reaching for on a mesh carried through
a sequence of poses, and worth little on a mesh transformed once and used once.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
matrix
|
mat44 | array[mat44]
|
|
required |
assume
|
str | None
|
A |
None
|
Returns:
| Type | Description |
|---|---|
Trimesh
|
New instance on the transformed vertices, sharing whatever cached values survive.
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Notes
Carried values are aliased, not copied, exactly as with
with_vertices: the returned mesh and this one hold
the same array objects. That is safe because neither can mutate them, and it is why this
class offers no in-place transform -- an edit to a shared buffer would corrupt every mesh
derived from it, with nothing to invalidate them. Use
transform_points with out= for in-place work on
raw buffers.
Examples:
mesh = tw.Trimesh(v, f)
spun = mesh.transform(tw.transform.rotation_matrix((0.0, 0.0, 1.0), 0.5, mesh.centroid))
spun.cotmatrix # carried from `mesh`, not reassembled
See Also
triwarp.transform.transform_mesh
The buffer-level form, without the cache.
with_vertices
invalidate
Source code in triwarp/mesh.py
1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 | |
invert
invert() -> Trimesh
Return a new Trimesh with every face's winding reversed, flipping the surface's outside.
Positions are untouched and shared, so this changes only which side of the surface is the
outside: normals point the other way and the enclosed
volume changes sign. faces is a new buffer.
Because nothing moves, the cache survives this far better than any transform does -- every
quantity of the point set itself is carried (areas, the box, the centroid, edge lengths)
along with the whole undirected topology and the assembled
cotmatrix. What goes is the orientation-dependent half:
the directed edge tables, vertex_one_rings, the
per-corner tables, and everything reading a sign. The two normal buffers are negated
rather than dropped, which is exact and saves rebuilding vertex_normals from the
face_angles this drops.
Cheaper than reversing the buffer and rebuilding a Trimesh around it: nothing here has to
be rotated or reassembled, since positions do not move -- only the two normal buffers are
touched, and by negation rather than recomputation.
Returns:
| Type | Description |
|---|---|
Trimesh
|
New instance sharing |
Notes
An involution up to the cache: mesh.invert().invert() has the same buffers' contents as
mesh.
This is not repair.make_normals_outward. That one
decides per connected component and leaves an already-outward mesh alone; this flips
unconditionally, so it turns a solid inside out.
Examples:
mesh = tw.Trimesh(v, f)
flipped = mesh.invert()
assert flipped.volume < 0.0 < mesh.volume # what was the outside is now the inside
See Also
triwarp.repair.reverse_winding
The buffer-level form, without the cache.
triwarp.repair.make_normals_outward
transform
Also reverses winding, when its matrix mirrors.
trimesh.Trimesh.invert
The same operation under trimesh's name for it, which mutates in place (no Sphinx
inventory entry to link).
Source code in triwarp/mesh.py
1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 | |
with_vertices
with_vertices(new_vertices: array[vec3]) -> Trimesh
Return a new Trimesh with different vertex positions but the same topology.
Cached quantities that depend only on faces (edges, adjacency, boundary,
manifold/orientation predicates) are carried forward; quantities that depend on
vertex positions (normals, areas, angles, the BVH, ...) are dropped and recomputed
lazily on the new instance. faces is shared (aliased), not copied.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
new_vertices
|
array[vec3]
|
|
required |
Returns:
| Type | Description |
|---|---|
Trimesh
|
New instance sharing |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in triwarp/mesh.py
1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 | |
with_faces
with_faces(new_faces: array[int32]) -> Trimesh
Return a new Trimesh with different faces; no cached quantities are carried forward.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
new_faces
|
array[int32]
|
Replacement triangle indices, flat or |
required |
Returns:
| Type | Description |
|---|---|
Trimesh
|
New instance sharing |
Source code in triwarp/mesh.py
1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 | |
submesh
submesh(faces: array[int32] | array[bool]) -> Trimesh
Extract the selected faces as a new Trimesh with vertices reindexed from zero.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
faces
|
array[int32] | array[bool]
|
Either a |
required |
Returns:
| Type | Description |
|---|---|
Trimesh
|
Compact submesh with an empty cache. No cached value is carried: the vertex numbering changes, so every index-valued quantity on this mesh names different vertices there. |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
See Also
triwarp.selection.submesh_from_face_indices
triwarp.selection.submesh_from_face_mask
split
trimesh.Trimesh.submesh
Source code in triwarp/mesh.py
1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 | |
split
split(*, copy: bool = False) -> list[Trimesh]
Split into connected components by face adjacency, as one Trimesh per body.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
copy
|
bool
|
Give each component independent buffers. By default the components are views into
two shared allocations, so holding one keeps both alive -- the
|
False
|
Returns:
| Type | Description |
|---|---|
list[Trimesh]
|
One mesh per body, each with vertices reindexed from zero and an empty cache. The
count is |
See Also
triwarp.combine.split
body_count
The same number without materializing the bodies.
trimesh.Trimesh.split
Source code in triwarp/mesh.py
1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 | |
copy
copy() -> Trimesh
Return a Trimesh on independent copies of vertices and faces, with an empty cache.
Returns:
| Type | Description |
|---|---|
Trimesh
|
New instance sharing nothing with this one. |
Notes
Reach for this before deliberately rewriting a buffer from a kernel: every other method here aliases its buffers into the meshes it returns, so an in-place edit would reach them too. Where the mesh is only being read, the copy is pure cost -- nothing on this class mutates.
See Also
Source code in triwarp/mesh.py
1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 | |
__add__
__add__(other: Trimesh) -> Trimesh
Concatenate two meshes into one, offsetting the second's face indices.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
Trimesh
|
Mesh to append; its vertices follow this mesh's in the result. |
required |
Returns:
| Type | Description |
|---|---|
Trimesh
|
Combined mesh with an empty cache. Vertices are not merged, so a shared surface
stays two coincident sheets -- |
See Also
triwarp.combine.concatenate
split
The inverse, up to ordering.
trimesh.Trimesh.__add__
The same operator under trimesh's name for it (no Sphinx inventory entry to link).
Source code in triwarp/mesh.py
1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 | |
invalidate
invalidate() -> None
Clear every cached value, including the warp_mesh BVH.
Call this after mutating vertices or faces in place from a kernel — Trimesh has
no way to detect such an edit on its own, so cached values would otherwise silently
go stale.
Source code in triwarp/mesh.py
1984 1985 1986 1987 1988 1989 1990 1991 1992 | |