Skip to content

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]

(n_vertices,) mesh vertex positions.

required
faces array[int32]

Triangle indices as wp.int32, either flat (length 3 * n_faces) or shape (n_faces, 3) (flattened on construction).

required
initial_cache dict[str, object] | None

Optional pre-populated cache entries, keyed by property name. Used by from_warp_mesh to seed warp_mesh without rebuilding its BVH, and by with_vertices to carry forward topology-only caches.

None

Raises:

Type Description
TypeError

If vertices is not a rank-1 wp.vec3 array, or faces is not rank-1 or rank-2 wp.int32 after flattening.

ValueError

If a rank-2 faces array does not have shape (n_faces, 3), or the flattened face buffer's size is not a multiple of 3.

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

trimesh.Trimesh

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
def __init__(
    self,
    vertices: wp.array[wp.vec3],
    faces: wp.array[wp.int32],
    *,
    initial_cache: dict[str, object] | None = None,
) -> None:
    """Construct a `Trimesh` from a vertex buffer and a face index buffer."""
    twt.ensure_ndim(vertices, 1, dtype=wp.vec3)
    if int(faces.ndim) == 2:
        if int(faces.shape[1]) != 3:
            raise ValueError(f"2D faces must have shape (n_faces, 3), got {tuple(faces.shape)}")
        faces = faces.reshape((-1,))
    twt.ensure_ndim(faces, 1, dtype=wp.int32)
    if int(faces.size) % 3 != 0:
        raise ValueError(f"faces size must be a multiple of 3, got {int(faces.size)}")

    self._vertices = vertices.contiguous()
    self._faces = faces.contiguous()
    self._cache: dict[str, object] = dict(initial_cache) if initial_cache is not None else {}

__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; mesh.points and mesh.indices become the new Trimesh's vertices and faces.

required

Returns:

Type Description
Trimesh

New instance whose warp_mesh property is mesh itself.

Source code in triwarp/mesh.py
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
@classmethod
def from_warp_mesh(cls, mesh: wp.Mesh) -> Trimesh:
    """
    Wrap an existing `warp.Mesh`, seeding `warp_mesh` so its BVH is never rebuilt.

    Parameters
    ----------
    mesh
        Source mesh; ``mesh.points`` and ``mesh.indices`` become the new `Trimesh`'s
        `vertices` and `faces`.

    Returns
    -------
    Trimesh
        New instance whose `warp_mesh` property is `mesh` itself.
    """
    return cls(mesh.points, mesh.indices, initial_cache={"warp_mesh": mesh})

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 warp.Mesh with an empty BVH silently corrupts CUDA state.

See Also

triwarp.ray triwarp.proximity

Source code in triwarp/mesh.py
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
@_CachedProperty
def warp_mesh(self) -> wp.Mesh:
    """
    Build the `vertices` / `faces` pair into a `warp.Mesh`, constructing its BVH.

    Raises
    ------
    ValueError
        If the mesh has zero faces — building a ``warp.Mesh`` with an empty BVH silently
        corrupts CUDA state.

    See Also
    --------
    [`triwarp.ray`][triwarp.ray]
    [`triwarp.proximity`][triwarp.proximity]
    """
    require_nonempty_mesh(self._faces, "Trimesh.warp_mesh")
    return wp.Mesh(points=self._vertices, indices=self._faces)

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.

See Also

face_normals_and_areas trimesh.Trimesh.face_normals

Source code in triwarp/mesh.py
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
@_CachedProperty
def face_normals(self) -> wp.array[wp.vec3]:
    """
    Length-``n_faces`` unit face normals (zero for degenerate faces).

    Computed together with [`face_areas`][triwarp.mesh.Trimesh.face_areas]; whichever is
    accessed first also caches the other.

    See Also
    --------
    [`face_normals_and_areas`][triwarp.triangles.face_normals_and_areas]
    [`trimesh.Trimesh.face_normals`][]
    """
    normals, areas = tw.triangles.face_normals_and_areas(self._vertices, self._faces)
    self._cache.setdefault("face_areas", areas)
    return normals

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.

See Also

face_normals_and_areas trimesh.Trimesh.area_faces

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
@_CachedProperty
def face_areas(self) -> wp.array[wp.float32]:
    """
    Length-``n_faces`` triangle areas.

    Computed together with [`face_normals`][triwarp.mesh.Trimesh.face_normals]; whichever
    is accessed first also caches the other.

    See Also
    --------
    [`face_normals_and_areas`][triwarp.triangles.face_normals_and_areas]
    [`trimesh.Trimesh.area_faces`][]
    """
    # Compute the pair rather than reading the slot `face_normals` would have filled: that
    # only happens when `face_normals` is itself cold, and it can be cached *alone* -- a
    # rigid `transform` rotates it while dropping the areas a scale would have changed.
    # `setdefault` so an already-cached (carried or rotated) normal array is kept.
    normals, areas = tw.triangles.face_normals_and_areas(self._vertices, self._faces)
    self._cache.setdefault("face_normals", normals)
    return areas

area

area() -> float

Total surface area (sum of face_areas).

Notes

Triggers a device-to-host synchronization on first access.

See Also

trimesh.Trimesh.area

Source code in triwarp/mesh.py
468
469
470
471
472
473
474
475
476
477
478
479
480
481
@_CachedProperty
def area(self) -> float:
    """
    Total surface area (sum of `face_areas`).

    Notes
    -----
    Triggers a device-to-host synchronization on first access.

    See Also
    --------
    [`trimesh.Trimesh.area`][]
    """
    return tw.reduce.sum(cast("twt.Array1dFloat32", self.face_areas))

face_angles

face_angles() -> twt.Array2dFloat32

Shape (n_faces, 3) interior angles in radians, aligned with each face's corners.

See Also

triwarp.triangles.face_angles trimesh.Trimesh.face_angles

Source code in triwarp/mesh.py
483
484
485
486
487
488
489
490
491
492
493
@_CachedProperty
def face_angles(self) -> twt.Array2dFloat32:
    """
    Shape ``(n_faces, 3)`` interior angles in radians, aligned with each face's corners.

    See Also
    --------
    [`triwarp.triangles.face_angles`][]
    [`trimesh.Trimesh.face_angles`][]
    """
    return tw.triangles.face_angles(self._vertices, self._faces)

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
@_CachedProperty
def triangles_center(self) -> 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`][triwarp.triangles.face_centroids].

    See Also
    --------
    [`triwarp.triangles.face_centroids`][]
    [`centroid`][triwarp.mesh.Trimesh.centroid]
        The area-weighted centre of the whole surface, which is not the mean of these.
    """
    return tw.triangles.face_centroids(self._vertices, self._faces)

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.

See Also

triwarp.measures.surface_centroid trimesh.Trimesh.centroid

Source code in triwarp/mesh.py
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
@_CachedProperty
def centroid(self) -> 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.

    See Also
    --------
    [`triwarp.measures.surface_centroid`][]
    [`trimesh.Trimesh.centroid`][]
    """
    return tw.measures.surface_centroid(self._vertices, self._faces)

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
@_CachedProperty
def volume(self) -> 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`][triwarp.mesh.Trimesh.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`][triwarp.mesh.Trimesh.is_volume]
        The check for the precondition this quantity needs.
    [`trimesh.Trimesh.volume`][]
    """
    return tw.measures.volume(self._vertices, self._faces)

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).

See Also

triwarp.measures.moments centroid trimesh.Trimesh.center_mass

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
@_CachedProperty
def center_mass(self) -> wp.vec3:
    """
    Centre of mass of the enclosed solid at unit density (all-``NaN`` for an empty mesh).

    **Not** [`centroid`][triwarp.mesh.Trimesh.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`][triwarp.mesh.Trimesh.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).

    See Also
    --------
    [`triwarp.measures.moments`][]
    [`centroid`][triwarp.mesh.Trimesh.centroid]
    [`trimesh.Trimesh.center_mass`][]
    """
    _volume, center, inertia = tw.measures.moments(self._vertices, self._faces)
    self._cache.setdefault("moment_inertia", inertia)
    return center

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.

See Also

triwarp.measures.moments center_mass trimesh.Trimesh.moment_inertia

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
@_CachedProperty
def moment_inertia(self) -> 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`][triwarp.mesh.Trimesh.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`][triwarp.mesh.Trimesh.transform] carries it through one.

    See Also
    --------
    [`triwarp.measures.moments`][]
    [`center_mass`][triwarp.mesh.Trimesh.center_mass]
    [`trimesh.Trimesh.moment_inertia`][]
    """
    _volume, center, inertia = tw.measures.moments(self._vertices, self._faces)
    self._cache.setdefault("center_mass", center)
    return inertia

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

triwarp.bounds.aabb enclosing_diagonal

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
@_CachedProperty
def bounds(self) -> 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`][triwarp.levelset.marching_cubes],
    [`signed_distance_grid`][triwarp.proximity.signed_distance_grid],
    [`query_nearest`][triwarp.neighbors.query_nearest],
    [`grid_points`][triwarp.voxels.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`][triwarp.bounds.aabb] convention.

    See Also
    --------
    [`triwarp.bounds.aabb`][]
    [`enclosing_diagonal`][triwarp.mesh.Trimesh.enclosing_diagonal]
    """
    return tw.bounds.aabb(self._vertices)

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.

See Also

bounds enclosing_diagonal trimesh.Trimesh.extents

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
@_CachedProperty
def extents(self) -> wp.vec3:
    """
    Side lengths of the axis-aligned [`bounds`][triwarp.mesh.Trimesh.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`][triwarp.mesh.Trimesh.bounds]' ``(+inf, -inf)``
    convention.

    See Also
    --------
    [`bounds`][triwarp.mesh.Trimesh.bounds]
    [`enclosing_diagonal`][triwarp.mesh.Trimesh.enclosing_diagonal]
    [`trimesh.Trimesh.extents`][]
    """
    lower, upper = self.bounds
    return upper - lower

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
@_CachedProperty
def enclosing_diagonal(self) -> float:
    """
    Diagonal length of `bounds`: this package's default mesh-query search radius.

    Notes
    -----
    Costs no device work of its own, unlike
    [`enclosing_diagonal`][triwarp.bounds.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`][triwarp.mesh.Trimesh.bounds]
    ``trimesh.Trimesh.scale``
        The same quantity under trimesh's name for it (no Sphinx inventory entry to link).
    """
    lower, upper = self.bounds
    return float(wp.length(upper - lower))

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
@_CachedProperty
def vertex_normals(self) -> 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`][triwarp.vertices.vertex_normals] at ``weighting="angle"``
    [`trimesh.Trimesh.vertex_normals`][]
    """
    return tw.vertices.vertex_normals(
        self._vertices,
        self._faces,
        weighting="angle",
        face_normals=self.face_normals,
        face_weights=self.face_angles,
    )

vertex_defects

vertex_defects() -> wp.array[wp.float32]

Length-n_vertices discrete angle defect (2π minus incident corner angles).

See Also

triwarp.vertices.vertex_defects

Source code in triwarp/mesh.py
694
695
696
697
698
699
700
701
702
703
@_CachedProperty
def vertex_defects(self) -> wp.array[wp.float32]:
    """
    Length-``n_vertices`` discrete angle defect (``2π`` minus incident corner angles).

    See Also
    --------
    [`triwarp.vertices.vertex_defects`][]
    """
    return tw.vertices.vertex_defects(self.n_vertices, self._faces, self.face_angles)

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

face_nondegenerate_mask

Source code in triwarp/mesh.py
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
@_CachedProperty
def nondegenerate_faces(self) -> 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`][triwarp.triangles.face_nondegenerate_mask].

    See Also
    --------
    [`face_nondegenerate_mask`][triwarp.triangles.face_nondegenerate_mask]
    """
    return tw.triangles.face_nondegenerate_mask(self._vertices, self._faces)

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

triwarp.edges.mean_edge_length

Source code in triwarp/mesh.py
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
@_CachedProperty
def mean_edge_length(self) -> 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
    --------
    [`triwarp.edges.mean_edge_length`][]
    """
    if self.n_faces == 0:
        return 0.0
    lengths = tw.edges.edges_length(self._vertices, self._faces, edges_in=self.edges)
    return tw.reduce.mean(cast("twt.Array1dFloat32", lengths))

edges

edges() -> twt.Array2dInt32

Shape (n_faces * 3, 2) directed triangle edges, in face-corner order.

See Also

triwarp.edges.faces_to_edges trimesh.Trimesh.edges

Source code in triwarp/mesh.py
739
740
741
742
743
744
745
746
747
748
749
@_CachedProperty
def edges(self) -> twt.Array2dInt32:
    """
    Shape ``(n_faces * 3, 2)`` directed triangle edges, in face-corner order.

    See Also
    --------
    [`triwarp.edges.faces_to_edges`][]
    [`trimesh.Trimesh.edges`][]
    """
    return tw.edges.faces_to_edges(self._faces, sorted=False)

edges_sorted

edges_sorted() -> twt.Array2dInt32

Shape (n_faces * 3, 2) edges from edges, each row sorted (smaller index first).

See Also

triwarp.edges.faces_to_edges trimesh.Trimesh.edges_sorted

Source code in triwarp/mesh.py
751
752
753
754
755
756
757
758
759
760
761
@_CachedProperty
def edges_sorted(self) -> twt.Array2dInt32:
    """
    Shape ``(n_faces * 3, 2)`` edges from `edges`, each row sorted (smaller index first).

    See Also
    --------
    [`triwarp.edges.faces_to_edges`][]
    [`trimesh.Trimesh.edges_sorted`][]
    """
    return tw.edges.faces_to_edges(self._faces, sorted=True)

edges_face

edges_face() -> wp.array[wp.int32]

Length-n_faces * 3 face index for each row of edges / edges_sorted.

See Also

triwarp.edges.edges_face trimesh.Trimesh.edges_face

Source code in triwarp/mesh.py
763
764
765
766
767
768
769
770
771
772
773
@_CachedProperty
def edges_face(self) -> wp.array[wp.int32]:
    """
    Length-``n_faces * 3`` face index for each row of `edges` / `edges_sorted`.

    See Also
    --------
    [`triwarp.edges.edges_face`][]
    [`trimesh.Trimesh.edges_face`][]
    """
    return tw.edges.edges_face(self._faces)

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.

See Also

triwarp.edges.edges_unique trimesh.Trimesh.edges_unique

Source code in triwarp/mesh.py
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
@_CachedProperty
def edges_unique(self) -> twt.Array2dInt32:
    """
    Shape ``(m, 2)`` unique undirected edges, ``m <= n_faces * 3``.

    Also caches [`edges_unique_inverse`][triwarp.mesh.Trimesh.edges_unique_inverse] as a
    by-product.

    See Also
    --------
    [`triwarp.edges.edges_unique`][]
    [`trimesh.Trimesh.edges_unique`][]
    """
    unique, inverse = tw.edges.edges_unique(
        self._faces, edges_sorted=self.edges_sorted, n_vertices=self.n_vertices
    )
    self._cache.setdefault("edges_unique_inverse", inverse)
    return unique

edges_unique_inverse

edges_unique_inverse() -> wp.array[wp.int32]

Length-n_faces * 3 inverse mapping into edges_unique (reconstructs edges_sorted).

See Also

triwarp.edges.edges_unique_inverse trimesh.Trimesh.edges_unique_inverse

Source code in triwarp/mesh.py
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
@_CachedProperty
def edges_unique_inverse(self) -> wp.array[wp.int32]:
    """
    Length-``n_faces * 3`` inverse mapping into `edges_unique` (reconstructs `edges_sorted`).

    See Also
    --------
    [`triwarp.edges.edges_unique_inverse`][]
    [`trimesh.Trimesh.edges_unique_inverse`][]
    """
    # As in `face_areas`: `edges_unique` fills this only when it is itself cold, and a
    # mirroring `transform` carries it while dropping this one, whose row order reverses.
    unique, inverse = tw.edges.edges_unique(
        self._faces, edges_sorted=self.edges_sorted, n_vertices=self.n_vertices
    )
    self._cache.setdefault("edges_unique", unique)
    return inverse

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.

See Also

edges_unique_inverse trimesh.Trimesh.faces_unique_edges

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
@_CachedProperty
def faces_unique_edges(self) -> twt.Array2dInt32:
    """
    Shape ``(n_faces, 3)`` index into [`edges_unique`][triwarp.mesh.Trimesh.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`][triwarp.mesh.Trimesh.edges_unique_inverse] viewed as ``(n, 3)``,
    and shares that buffer rather than copying it.

    See Also
    --------
    [`edges_unique_inverse`][triwarp.mesh.Trimesh.edges_unique_inverse]
    [`trimesh.Trimesh.faces_unique_edges`][]
    """
    return twt.as_array2d(self.edges_unique_inverse.reshape((-1, 3)), wp.int32)

edges_unique_length

edges_unique_length() -> wp.array[wp.float32]

Length-m Euclidean length of each edges_unique row.

See Also

triwarp.edges.edges_unique_length trimesh.Trimesh.edges_unique_length

Source code in triwarp/mesh.py
833
834
835
836
837
838
839
840
841
842
843
844
845
@_CachedProperty
def edges_unique_length(self) -> wp.array[wp.float32]:
    """
    Length-``m`` Euclidean length of each `edges_unique` row.

    See Also
    --------
    [`triwarp.edges.edges_unique_length`][]
    [`trimesh.Trimesh.edges_unique_length`][]
    """
    return tw.edges.edges_unique_length(
        self._vertices, self._faces, unique_edges=self.edges_unique
    )

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.

See Also

triwarp.adjacency.face_adjacency trimesh.graph.face_adjacency

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
@_CachedProperty
def face_adjacency(self) -> twt.Array2dInt32:
    """
    Shape ``(m, 2)`` face index pairs that share an undirected edge.

    Also caches [`face_adjacency_edges`][triwarp.mesh.Trimesh.face_adjacency_edges] as a
    by-product.

    See Also
    --------
    [`triwarp.adjacency.face_adjacency`][]
    [`trimesh.graph.face_adjacency`][]
    """
    adjacency, adjacency_edges = tw.adjacency.face_adjacency(
        self._faces,
        edges_sorted=self.edges_sorted,
        return_edges=True,
        # The mesh knows its own vertex count, so the row-hash radix never has to be inferred
        # from a device reduction ending in a host readback.
        n_vertices=int(self._vertices.shape[0]),
    )
    self._cache.setdefault("face_adjacency_edges", adjacency_edges)
    return adjacency

face_adjacency_edges

face_adjacency_edges() -> twt.Array2dInt32

Shape (m, 2) shared vertex pair for each face_adjacency row.

See Also

triwarp.adjacency.face_adjacency

Source code in triwarp/mesh.py
871
872
873
874
875
876
877
878
879
880
881
882
883
884
@_CachedProperty
def face_adjacency_edges(self) -> twt.Array2dInt32:
    """
    Shape ``(m, 2)`` shared vertex pair for each `face_adjacency` row.

    See Also
    --------
    [`triwarp.adjacency.face_adjacency`][]
    """
    _ = self.face_adjacency
    # Both halves live in `_TOPOLOGY_KEYS`, so nothing splits this pair today; the read stays
    # a plain one rather than recomputing, and `face_adjacency` uses `setdefault` so a future
    # stratum that does split it fails loudly here instead of silently returning a stale edge.
    return cast("twt.Array2dInt32", self._cache["face_adjacency_edges"])

face_adjacency_unshared

face_adjacency_unshared() -> twt.Array2dInt32

Shape (m, 2) vertex on each adjacent face not on their shared edge.

See Also

triwarp.adjacency.face_adjacency_unshared trimesh.graph.face_adjacency_unshared

Source code in triwarp/mesh.py
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
@_CachedProperty
def face_adjacency_unshared(self) -> twt.Array2dInt32:
    """
    Shape ``(m, 2)`` vertex on each adjacent face not on their shared edge.

    See Also
    --------
    [`triwarp.adjacency.face_adjacency_unshared`][]
    [`trimesh.graph.face_adjacency_unshared`][]
    """
    return tw.adjacency.face_adjacency_unshared(
        self._faces,
        face_adjacency=self.face_adjacency,
        face_adjacency_edges=self.face_adjacency_edges,
    )

face_adjacency_angles

face_adjacency_angles() -> wp.array[wp.float32]

Length-m unsigned angle in radians between each face_adjacency pair.

See Also

triwarp.adjacency.face_adjacency_angles trimesh.Trimesh.face_adjacency_angles

Source code in triwarp/mesh.py
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
@_CachedProperty
def face_adjacency_angles(self) -> wp.array[wp.float32]:
    """
    Length-``m`` unsigned angle in radians between each `face_adjacency` pair.

    See Also
    --------
    [`triwarp.adjacency.face_adjacency_angles`][]
    [`trimesh.Trimesh.face_adjacency_angles`][]
    """
    return tw.adjacency.face_adjacency_angles(
        self._vertices,
        self._faces,
        face_adjacency=self.face_adjacency,
        face_normals=self.face_normals,
    )

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
@_CachedProperty
def face_adjacency_projections(self) -> 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`][triwarp.mesh.Trimesh.face_adjacency_convex] thresholds.

    See Also
    --------
    [`triwarp.adjacency.face_adjacency_projections`][]
    [`face_adjacency_convex`][triwarp.mesh.Trimesh.face_adjacency_convex]
    [`trimesh.Trimesh.face_adjacency_projections`][]
    """
    return tw.adjacency.face_adjacency_projections(
        self._vertices,
        self._faces,
        face_adjacency=self.face_adjacency,
        face_adjacency_edges=self.face_adjacency_edges,
        face_adjacency_unshared=self.face_adjacency_unshared,
        face_normals=self.face_normals,
    )

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
@_CachedProperty
def face_adjacency_convex(self) -> 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`][triwarp.mesh.Trimesh.face_adjacency_projections]
    [`trimesh.Trimesh.face_adjacency_convex`][]
    """
    return tw.adjacency.face_adjacency_convex(
        self._vertices,
        self._faces,
        face_adjacency=self.face_adjacency,
        face_adjacency_edges=self.face_adjacency_edges,
        face_adjacency_unshared=self.face_adjacency_unshared,
        face_normals=self.face_normals,
    )

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.

See Also

triwarp.adjacency.face_connected_component_labels

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
@_CachedProperty
def face_connected_component_labels(self) -> wp.array[wp.int32]:
    """
    Length-``n_faces`` connected-component label per face (face-adjacency graph).

    Notes
    -----
    Calls
    [`connected_component_labels_from_edges`][triwarp.graph.connected_component_labels_from_edges]
    directly rather than going through
    [`face_connected_component_labels`][triwarp.adjacency.face_connected_component_labels], so
    that the cached `face_adjacency` is reused instead of rebuilt. Same labelling engine, same
    answer.

    See Also
    --------
    [`triwarp.adjacency.face_connected_component_labels`][]
    """
    return tw.graph.connected_component_labels_from_edges(
        self.face_adjacency, node_count=self.n_faces
    )

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
@_CachedProperty
def body_count(self) -> 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.mesh.Trimesh.face_connected_component_labels]
    [`triwarp.combine.split`][]
        Materializes the bodies this counts.
    [`trimesh.Trimesh.body_count`][]
    """
    if self.n_faces == 0:
        return 0
    return int(tw.grouping.unique_1d(self.face_connected_component_labels).shape[0])

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.

See Also

triwarp.adjacency.vertex_face_adjacency vertex_one_rings

Source code in triwarp/mesh.py
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
@_CachedProperty
def vertex_face_adjacency(self) -> 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.

    See Also
    --------
    [`triwarp.adjacency.vertex_face_adjacency`][]
    [`vertex_one_rings`][triwarp.mesh.Trimesh.vertex_one_rings]
    """
    return tw.adjacency.vertex_face_adjacency(self._faces, n_vertices=self.n_vertices)

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 halfedge_twins when an undirected edge carries three or more halfedges, i.e. the mesh is not edge-manifold.

Notes

Triggers a device-to-host synchronization on first access (that manifoldness check).

See Also

triwarp.halfedge.halfedge_twins vertex_one_rings

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
@_CachedProperty
def halfedge_twins(self) -> 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
    ------
    ValueError
        Propagated from [`halfedge_twins`][triwarp.halfedge.halfedge_twins] when an undirected
        edge carries three or more halfedges, i.e. the mesh is not edge-manifold.

    Notes
    -----
    Triggers a device-to-host synchronization on first access (that manifoldness check).

    See Also
    --------
    [`triwarp.halfedge.halfedge_twins`][]
    [`vertex_one_rings`][triwarp.mesh.Trimesh.vertex_one_rings]
    """
    return tw.halfedge.halfedge_twins(self._faces, n_vertices=self.n_vertices)

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 vertex_one_rings when a vertex's rotation closes before its whole fan is covered (a pinched, vertex-non-manifold vertex), or from halfedge_twins on an edge-non-manifold mesh.

Notes

Triggers a device-to-host synchronization on first access (that manifoldness check).

See Also

triwarp.halfedge.vertex_one_rings halfedge_twins vertex_face_adjacency

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
@_CachedProperty
def vertex_one_rings(self) -> 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
    ------
    ValueError
        Propagated from [`vertex_one_rings`][triwarp.halfedge.vertex_one_rings] when a vertex's
        rotation closes before its whole fan is covered (a pinched, vertex-non-manifold
        vertex), or from `halfedge_twins` on an edge-non-manifold mesh.

    Notes
    -----
    Triggers a device-to-host synchronization on first access (that manifoldness check).

    See Also
    --------
    [`triwarp.halfedge.vertex_one_rings`][]
    [`halfedge_twins`][triwarp.mesh.Trimesh.halfedge_twins]
    [`vertex_face_adjacency`][triwarp.mesh.Trimesh.vertex_face_adjacency]
    """
    return tw.halfedge.vertex_one_rings(
        self._faces, twins=self.halfedge_twins, n_vertices=self.n_vertices
    )

boundary_edges

boundary_edges() -> twt.Array2dInt32

Shape (n_boundary, 2) undirected boundary edges (each row sorted, min-first).

See Also

triwarp.boundary.boundary_edges

Source code in triwarp/mesh.py
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
@_CachedProperty
def boundary_edges(self) -> twt.Array2dInt32:
    """
    Shape ``(n_boundary, 2)`` undirected boundary edges (each row sorted, min-first).

    See Also
    --------
    [`triwarp.boundary.boundary_edges`][]
    """
    return tw.boundary.boundary_edges(self._vertices, self._faces, self.edges_sorted)

oriented_boundary_edges

oriented_boundary_edges() -> twt.Array2dInt32

Shape (n_boundary, 2) directed boundary edges, preserving face winding.

See Also

triwarp.boundary.oriented_boundary_edges

Source code in triwarp/mesh.py
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
@_CachedProperty
def oriented_boundary_edges(self) -> twt.Array2dInt32:
    """
    Shape ``(n_boundary, 2)`` directed boundary edges, preserving face winding.

    See Also
    --------
    [`triwarp.boundary.oriented_boundary_edges`][]
    """
    return tw.boundary.oriented_boundary_edges(
        self._vertices, self._faces, self.edges_sorted, self.edges
    )

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

triwarp.boundary.boundary_loops

Source code in triwarp/mesh.py
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
@_CachedProperty
def boundary_loops(self) -> list[wp.array[wp.int32]]:
    """
    One ordered vertex-index array per boundary loop (empty list if the mesh is closed).

    See Also
    --------
    [`triwarp.boundary.boundary_loops`][]
    """
    return tw.boundary.boundary_loops(
        self._vertices, self._faces, self.edges_sorted, self.edges
    )

boundary_vertex_indices

boundary_vertex_indices() -> wp.array[wp.int32]

Sorted unique vertex indices lying on the mesh boundary.

See Also

triwarp.boundary.boundary_vertex_indices

Source code in triwarp/mesh.py
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
@_CachedProperty
def boundary_vertex_indices(self) -> wp.array[wp.int32]:
    """
    Sorted unique vertex indices lying on the mesh boundary.

    See Also
    --------
    [`triwarp.boundary.boundary_vertex_indices`][]
    """
    return tw.boundary.boundary_vertex_indices(self._vertices, self._faces, self.edges_sorted)

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.

See Also

triwarp.measures.euler_characteristic trimesh.Trimesh.euler_number

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
@_CachedProperty
def euler_characteristic(self) -> 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`][triwarp.measures.euler_characteristic], recomposed here to
    reuse the cached `edges_unique` count instead of recomputing it.

    See Also
    --------
    [`triwarp.measures.euler_characteristic`][]
    [`trimesh.Trimesh.euler_number`][]
    """
    if self.n_faces == 0:
        return 0
    n_referenced = int(tw.grouping.unique_1d(self._faces).shape[0])
    return n_referenced - int(self.edges_unique.shape[0]) + self.n_faces

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

triwarp.validation.is_edge_manifold

Source code in triwarp/mesh.py
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
@_CachedProperty
def is_edge_manifold(self) -> bool:
    """
    Whether every undirected edge is shared by one or two faces.

    Notes
    -----
    Triggers a device-to-host synchronization on first access.

    See Also
    --------
    [`triwarp.validation.is_edge_manifold`][]
    """
    return tw.validation.is_edge_manifold(
        self._faces, edges_sorted=self.edges_sorted, n_vertices=self.n_vertices
    )

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.

See Also

triwarp.validation.is_vertex_manifold

Source code in triwarp/mesh.py
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
@_CachedProperty
def is_vertex_manifold(self) -> bool:
    """
    Whether every referenced vertex has a single edge-connected fan of faces.

    Notes
    -----
    Triggers a device-to-host synchronization on first access.

    See Also
    --------
    [`triwarp.validation.is_vertex_manifold`][]
    """
    return tw.validation.is_vertex_manifold(
        self._faces,
        face_adjacency=self.face_adjacency,
        face_adjacency_edges=self.face_adjacency_edges,
    )

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.

See Also

triwarp.validation.is_winding_consistent trimesh.Trimesh.is_winding_consistent

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
@_CachedProperty
def is_winding_consistent(self) -> 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`][triwarp.validation.is_winding_consistent], recomposed here to
    reuse the cached `edges` / `edges_sorted`.

    See Also
    --------
    [`triwarp.validation.is_winding_consistent`][]
    [`trimesh.Trimesh.is_winding_consistent`][]
    """
    if self.n_faces == 0:
        return True
    mask = tw.validation.edge_winding_consistent_mask(
        self._faces, edges=self.edges, edges_sorted=self.edges_sorted
    )
    if int(mask.shape[0]) == 0:
        return True
    return bool(tw.reduce.all(mask))

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

triwarp.validation.is_orientable

Source code in triwarp/mesh.py
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
@_CachedProperty
def is_orientable(self) -> 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
    --------
    [`triwarp.validation.is_orientable`][]
    """
    return tw.validation.is_orientable(self._faces)

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.

See Also

triwarp.validation.is_watertight is_volume

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
@_CachedProperty
def is_watertight(self) -> 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`][triwarp.mesh.Trimesh.is_self_intersecting] gets
    for free by taking a `wp.Mesh` directly.

    See Also
    --------
    [`triwarp.validation.is_watertight`][]
    [`is_volume`][triwarp.mesh.Trimesh.is_volume]
    """
    if self.n_faces == 0:
        return True
    return tw.validation.is_watertight(
        self._vertices, self._faces, edges_sorted=self.edges_sorted, mesh=self.warp_mesh
    )

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 warp_mesh).

Notes

Triggers a device-to-host synchronization on first access. Reuses the cached warp_mesh BVH for its broad phase.

See Also

triwarp.validation.is_self_intersecting

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
@_CachedProperty
def is_self_intersecting(self) -> bool:
    """
    Whether any two non-adjacent triangles of the mesh intersect.

    Raises
    ------
    ValueError
        If the mesh has zero faces (see [`warp_mesh`][triwarp.mesh.Trimesh.warp_mesh]).

    Notes
    -----
    Triggers a device-to-host synchronization on first access. Reuses the cached
    `warp_mesh` BVH for its broad phase.

    See Also
    --------
    [`triwarp.validation.is_self_intersecting`][]
    """
    return tw.validation.is_self_intersecting(self.warp_mesh)

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.

See Also

triwarp.validation.is_volume trimesh.Trimesh.is_volume

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
@_CachedProperty
def is_volume(self) -> 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.

    See Also
    --------
    [`triwarp.validation.is_volume`][]
    [`trimesh.Trimesh.is_volume`][]
    """
    return tw.validation.is_volume(
        self._vertices, self._faces, edges=self.edges, edges_sorted=self.edges_sorted
    )

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.

See Also

triwarp.laplacian.cotmatrix_entries cotmatrix

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
@_CachedProperty
def cotmatrix_entries(self) -> 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`][triwarp.laplacian.cotmatrix],
    [`connection_laplacian`][triwarp.laplacian.connection_laplacian],
    [`heat_operators`][triwarp.heat.heat_operators] or
    [`crouzeix_raviart_cotmatrix`][triwarp.energies.crouzeix_raviart_cotmatrix] at either
    precision -- which is what this class's own ``float32`` `cotmatrix` and ``float64``
    `heat_operators` both do.

    See Also
    --------
    [`triwarp.laplacian.cotmatrix_entries`][]
    [`cotmatrix`][triwarp.mesh.Trimesh.cotmatrix]
    """
    return tw.laplacian.cotmatrix_entries(self._vertices, self._faces)

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
@_CachedProperty
def cotmatrix(self) -> 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`][triwarp.parametrization] assemble their own ``float64``
    operators, and `heat_operators` caches the one they share.

    See Also
    --------
    [`triwarp.laplacian.cotmatrix`][]
    [`cotmatrix_entries`][triwarp.mesh.Trimesh.cotmatrix_entries]
    [`mass_matrix_entries`][triwarp.mesh.Trimesh.mass_matrix_entries]
    [`triwarp.energies.k_harmonic`][]
        Takes this operator and `mass_matrix_entries` as its two arguments.
    """
    return tw.laplacian.cotmatrix(
        self._vertices, self._faces, cot_entries=self.cotmatrix_entries
    )

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.

See Also

triwarp.laplacian.mass_matrix_entries cotmatrix

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
@_CachedProperty
def mass_matrix_entries(self) -> 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`][triwarp.laplacian.mass_matrix_entries] at that dtype, which
    accumulates in the requested precision rather than casting this -- the two are not the same
    array widened.

    See Also
    --------
    [`triwarp.laplacian.mass_matrix_entries`][]
    [`cotmatrix`][triwarp.mesh.Trimesh.cotmatrix]
    """
    return tw.laplacian.mass_matrix_entries(
        self._vertices, self._faces, face_areas=self.face_areas
    )

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.

See Also

triwarp.laplacian.laplacian triwarp.smoothing.filter_laplacian cotmatrix

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
@_CachedProperty
def laplacian_operator(self) -> wps.BsrMatrix[wp.float32]:
    """
    Row-normalized 1-ring averaging operator (the uniform / umbrella Laplacian).

    Exactly what every position filter in [`triwarp.smoothing`][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`][triwarp.smoothing.filter_neighborhood_average] is the one
    filter that wants the *symmetric* adjacency instead
    ([`laplacian`][triwarp.laplacian.laplacian] at ``symmetric=True``) and must not be handed
    this one; the two differ only on a mesh with an open boundary.

    See Also
    --------
    [`triwarp.laplacian.laplacian`][]
    [`triwarp.smoothing.filter_laplacian`][]
    [`cotmatrix`][triwarp.mesh.Trimesh.cotmatrix]
    """
    return tw.laplacian.laplacian(self._vertices, self._faces)

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 vertex_one_rings on a non-manifold mesh.

See Also

triwarp.tangent_space.vertex_tangent_frames vector_heat_operators

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
@_CachedProperty
def vertex_tangent_frames(
    self,
) -> 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
    ------
    ValueError
        Propagated from `vertex_one_rings` on a non-manifold mesh.

    See Also
    --------
    [`triwarp.tangent_space.vertex_tangent_frames`][]
    [`vector_heat_operators`][triwarp.mesh.Trimesh.vector_heat_operators]
    """
    return tw.tangent_space.vertex_tangent_frames(
        self._vertices, self._faces, normals=self.vertex_normals, rings=self.vertex_one_rings
    )

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.

See Also

triwarp.heat.heat_operators vector_heat_operators

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
@_CachedProperty
def heat_operators(self) -> tw.heat.HeatOperators:
    """
    Source-independent operator bundle for the heat method, at the default diffusion time.

    Pass it to [`heat_geodesic`][triwarp.heat.heat_geodesic] or
    [`geodesic_path`][triwarp.geodesic_walk.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`][triwarp.heat.heat_operators] directly -- and can still pass this
    class's `cotmatrix_entries` into it.

    See Also
    --------
    [`triwarp.heat.heat_operators`][]
    [`vector_heat_operators`][triwarp.mesh.Trimesh.vector_heat_operators]
    """
    return tw.heat.heat_operators(
        self._vertices, self._faces, cot_entries=self.cotmatrix_entries
    )

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.

See Also

triwarp.heat.vector_heat_operators heat_operators vertex_tangent_frames

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
@_CachedProperty
def vector_heat_operators(self) -> 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`][triwarp.heat.transport_tangent_vectors],
    [`log_map`][triwarp.heat.log_map],
    [`extend_scalar`][triwarp.heat.extend_scalar] or
    [`heat_signed_distance`][triwarp.heat.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`][triwarp.heat.log_map]'s radius is asserted to *be* the
    [`heat_geodesic`][triwarp.heat.heat_geodesic] distance, so the two systems must
    share a diffusion time.

    See Also
    --------
    [`triwarp.heat.vector_heat_operators`][]
    [`heat_operators`][triwarp.mesh.Trimesh.heat_operators]
    [`vertex_tangent_frames`][triwarp.mesh.Trimesh.vertex_tangent_frames]
    """
    return tw.heat.vector_heat_operators(
        self._vertices,
        self._faces,
        scalar_operators=self.heat_operators,
        frames=self.vertex_tangent_frames,
    )

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]

(n,) query positions.

required

Returns:

Type Description
array[bool]

Length-n mask; True where the point is inside.

Raises:

Type Description
ValueError

If the mesh has zero faces (see warp_mesh).

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
def contains(self, points: wp.array[wp.vec3]) -> wp.array[wp.bool]:
    """
    Test which query points lie inside the mesh, by ray parity against the cached BVH.

    Parameters
    ----------
    points
        ``(n,)`` query positions.

    Returns
    -------
    wp.array[wp.bool]
        Length-``n`` mask; ``True`` where the point is inside.

    Raises
    ------
    ValueError
        If the mesh has zero faces (see [`warp_mesh`][triwarp.mesh.Trimesh.warp_mesh]).

    Notes
    -----
    Only meaningful for a closed, consistently wound surface --
    [`is_volume`][triwarp.mesh.Trimesh.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`][]
    """
    return tw.ray.contains_points(self.warp_mesh, points)

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.

None

Returns:

Name Type Description
points array[vec3]

(count,) sampled positions.

face_indices array[int32]

Length-count index of the face each sample landed on.

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
def sample(
    self, count: int, *, seed: int | None = None
) -> tuple[wp.array[wp.vec3], wp.array[wp.int32]]:
    """
    Sample points uniformly over the surface, area-weighted.

    Parameters
    ----------
    count
        Number of samples to draw.
    seed
        Seed for the sampler; non-reproducible when ``None``.

    Returns
    -------
    points : wp.array[wp.vec3]
        ``(count,)`` sampled positions.
    face_indices : wp.array[wp.int32]
        Length-``count`` index of the face each sample landed on.

    Notes
    -----
    Passes the cached [`face_areas`][triwarp.mesh.Trimesh.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`][]
    """
    return tw.sample.sample_surface(
        self._vertices, self._faces, count, face_weight=self.face_areas, seed=seed
    )

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]

4x4 transform, as a scalar wp.mat44 or a (1,) wp.array[wp.mat44] -- an icp result can be passed straight through. Build one with translation_matrix, rotation_matrix, scale_matrix or reflection_matrix.

required
assume str | None

A TransformKind value promising what matrix preserves, skipping the classification. Use it when a matrix composed from many float32 factors has drifted far enough off orthogonality to be classified TransformKind.AFFINE, which is safe but discards the cache. An incorrect promise silently corrupts every carried value -- there is no check.

None

Returns:

Type Description
Trimesh

New instance on the transformed vertices, sharing whatever cached values survive. self when matrix is the identity.

Raises:

Type Description
ValueError

If assume is not a TransformKind value.

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
def transform(
    self, matrix: wp.mat44 | wp.array[wp.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`][triwarp.transform.classify_transform]), and for a rigid motion
    that is everything expensive: angles, areas, the cotangent table and the assembled
    [`cotmatrix`][triwarp.mesh.Trimesh.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`][triwarp.mesh.Trimesh.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
    ----------
    matrix
        ``4x4`` transform, as a scalar ``wp.mat44`` or a ``(1,)`` ``wp.array[wp.mat44]`` --
        an [`icp`][triwarp.registration.icp] result can be passed straight through. Build one
        with [`translation_matrix`][triwarp.transform.translation_matrix],
        [`rotation_matrix`][triwarp.transform.rotation_matrix],
        [`scale_matrix`][triwarp.transform.scale_matrix] or
        [`reflection_matrix`][triwarp.transform.reflection_matrix].
    assume
        A [`TransformKind`][triwarp.transform.TransformKind] value promising what ``matrix``
        preserves, skipping the classification. Use it when a matrix composed from many
        ``float32`` factors has drifted far enough off orthogonality to be classified
        `TransformKind.AFFINE`, which is safe but discards the cache. **An incorrect promise
        silently corrupts every carried value** -- there is no check.

    Returns
    -------
    Trimesh
        New instance on the transformed vertices, sharing whatever cached values survive.
        ``self`` when ``matrix`` is the identity.

    Raises
    ------
    ValueError
        If ``assume`` is not a `TransformKind` value.

    Notes
    -----
    Carried values are **aliased**, not copied, exactly as with
    [`with_vertices`][triwarp.mesh.Trimesh.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`][triwarp.transform.transform_points] with ``out=`` for in-place work on
    raw buffers.

    Examples
    --------
    ```python
    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`][triwarp.mesh.Trimesh.with_vertices]
    [`invalidate`][triwarp.mesh.Trimesh.invalidate]
    """
    kind = str(
        tw.transform.TransformKind(assume)
        if assume is not None
        else tw.transform.classify_transform(matrix)
    )
    if kind == tw.transform.TransformKind.IDENTITY:
        return self

    # Every use below -- `transform_mesh`'s own `reverses_orientation` check, the explicit one
    # on the next line, up to four `transform_normals` calls in `_carry_directions`, up to two
    # `as_mat44` calls in `_carry_box` -- is a host branch over a matrix that does not change
    # between them. Resolving it once here (paying the one readback `classify_transform`/
    # `reverses_orientation` already forces when `matrix` is a device array) and passing the
    # resolved host value down turns what was up to 7-8 independent host syncs of the same 16
    # floats into exactly one.
    matrix_host = tw.transform.as_mat44(matrix)
    new_vertices, new_faces = tw.transform.transform_mesh(
        self._vertices, self._faces, matrix_host
    )
    carry = _TRANSFORM_CARRY[kind]
    if tw.transform.reverses_orientation(matrix_host):
        carry = carry - _ORIENTATION_DEPENDENT_KEYS
    survived = {key: value for key, value in self._cache.items() if key in carry}
    self._carry_directions(kind, matrix_host, survived)
    self._carry_box(kind, matrix_host, survived)
    return Trimesh(new_vertices, new_faces, initial_cache=survived)

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 vertices, on a reversed face buffer.

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
def invert(self) -> 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`][triwarp.mesh.Trimesh.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`][triwarp.mesh.Trimesh.cotmatrix]. What goes is the orientation-dependent half:
    the directed edge tables, [`vertex_one_rings`][triwarp.mesh.Trimesh.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
    -------
    Trimesh
        New instance sharing `vertices`, on a reversed face buffer.

    Notes
    -----
    An involution up to the cache: ``mesh.invert().invert()`` has the same buffers' contents as
    ``mesh``.

    This is not [`repair.make_normals_outward`][triwarp.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
    --------
    ```python
    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`][triwarp.mesh.Trimesh.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).
    """
    survived = {key: value for key, value in self._cache.items() if key in _INVERT_CARRY}
    for key in ("face_normals", "vertex_normals"):
        # Negated, not recomputed: exact, and it saves rebuilding `vertex_normals` from the
        # `face_angles` this flip drops.
        cached = self._cache.get(key)
        if cached is not None:
            normals = cast("wp.array[wp.vec3]", cached)
            flipped = wp.empty(int(normals.shape[0]), dtype=wp.vec3, device=normals.device)
            if int(normals.shape[0]) > 0:
                wp.map(wp.neg, normals, out=flipped)
            survived[key] = flipped
    return Trimesh(
        self._vertices, tw.repair.reverse_winding(self._faces), initial_cache=survived
    )

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]

(n_vertices,) replacement vertex positions; must have the same length as the current vertices.

required

Returns:

Type Description
Trimesh

New instance sharing faces with the topology-only caches carried forward.

Raises:

Type Description
ValueError

If new_vertices has a different vertex count than the current mesh.

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
def with_vertices(self, new_vertices: wp.array[wp.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
    ----------
    new_vertices
        ``(n_vertices,)`` replacement vertex positions; must have the same length as the
        current `vertices`.

    Returns
    -------
    Trimesh
        New instance sharing `faces` with the topology-only caches carried forward.

    Raises
    ------
    ValueError
        If ``new_vertices`` has a different vertex count than the current mesh.
    """
    if int(new_vertices.shape[0]) != self.n_vertices:
        raise ValueError(
            f"with_vertices requires the same vertex count ({self.n_vertices}), "
            f"got {int(new_vertices.shape[0])}"
        )
    survived = {key: value for key, value in self._cache.items() if key in _TOPOLOGY_KEYS}
    return Trimesh(new_vertices, self._faces, initial_cache=survived)

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 (n_faces, 3).

required

Returns:

Type Description
Trimesh

New instance sharing vertices with an empty cache.

Source code in triwarp/mesh.py
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
def with_faces(self, new_faces: wp.array[wp.int32]) -> Trimesh:
    """
    Return a new `Trimesh` with different faces; no cached quantities are carried forward.

    Parameters
    ----------
    new_faces
        Replacement triangle indices, flat or ``(n_faces, 3)``.

    Returns
    -------
    Trimesh
        New instance sharing `vertices` with an empty cache.
    """
    return Trimesh(self._vertices, new_faces)

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 wp.int32 array of face indices or a length-n_faces wp.bool mask; the dtype selects which.

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 faces is neither a wp.int32 nor a wp.bool array.

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
def submesh(self, faces: wp.array[wp.int32] | wp.array[wp.bool]) -> Trimesh:
    """
    Extract the selected faces as a new `Trimesh` with vertices reindexed from zero.

    Parameters
    ----------
    faces
        Either a ``wp.int32`` array of face indices or a length-``n_faces`` ``wp.bool`` mask;
        the dtype selects which.

    Returns
    -------
    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
    ------
    TypeError
        If ``faces`` is neither a ``wp.int32`` nor a ``wp.bool`` array.

    See Also
    --------
    [`triwarp.selection.submesh_from_face_indices`][]
    [`triwarp.selection.submesh_from_face_mask`][]
    [`split`][triwarp.mesh.Trimesh.split]
    [`trimesh.Trimesh.submesh`][]
    """
    if faces.dtype is wp.bool:
        parts = tw.selection.submesh_from_face_mask(self._vertices, self._faces, faces)
    elif faces.dtype is wp.int32:
        parts = tw.selection.submesh_from_face_indices(self._vertices, self._faces, faces)
    else:
        raise TypeError(f"submesh needs a wp.int32 or wp.bool array, got {faces.dtype}")
    return Trimesh(*parts)

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 triwarp.combine.split convention, carried through unchanged.

False

Returns:

Type Description
list[Trimesh]

One mesh per body, each with vertices reindexed from zero and an empty cache. The count is body_count.

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
def split(self, *, copy: bool = False) -> list[Trimesh]:
    """
    Split into connected components by face adjacency, as one `Trimesh` per body.

    Parameters
    ----------
    copy
        Give each component independent buffers. By default the components are **views** into
        two shared allocations, so holding one keeps both alive -- the
        [`triwarp.combine.split`][] convention, carried through unchanged.

    Returns
    -------
    list[Trimesh]
        One mesh per body, each with vertices reindexed from zero and an empty cache. The
        count is [`body_count`][triwarp.mesh.Trimesh.body_count].

    See Also
    --------
    [`triwarp.combine.split`][]
    [`body_count`][triwarp.mesh.Trimesh.body_count]
        The same number without materializing the bodies.
    [`trimesh.Trimesh.split`][]
    """
    return [
        Trimesh(vertices, faces)
        for vertices, faces in tw.combine.split(self._vertices, self._faces, copy=copy)
    ]

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

with_vertices invalidate trimesh.Trimesh.copy

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
def copy(self) -> Trimesh:
    """
    Return a `Trimesh` on independent copies of `vertices` and `faces`, with an empty cache.

    Returns
    -------
    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
    --------
    [`with_vertices`][triwarp.mesh.Trimesh.with_vertices]
    [`invalidate`][triwarp.mesh.Trimesh.invalidate]
    [`trimesh.Trimesh.copy`][]
    """
    return Trimesh(wp.clone(self._vertices), wp.clone(self._faces))

__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 -- triwarp.repair.remove_duplicated_vertices is the weld.

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
def __add__(self, other: Trimesh) -> Trimesh:
    """
    Concatenate two meshes into one, offsetting the second's face indices.

    Parameters
    ----------
    other
        Mesh to append; its vertices follow this mesh's in the result.

    Returns
    -------
    Trimesh
        Combined mesh with an empty cache. Vertices are **not** merged, so a shared surface
        stays two coincident sheets -- [`triwarp.repair.remove_duplicated_vertices`][] is the
        weld.

    See Also
    --------
    [`triwarp.combine.concatenate`][]
    [`split`][triwarp.mesh.Trimesh.split]
        The inverse, up to ordering.
    ``trimesh.Trimesh.__add__``
        The same operator under trimesh's name for it (no Sphinx inventory entry to link).
    """
    return Trimesh(
        *tw.combine.concatenate([(self._vertices, self._faces), (other.vertices, other.faces)])
    )

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
def invalidate(self) -> 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.
    """
    self._cache.clear()