Skip to content

adjacency

triwarp.adjacency

Mesh face-adjacency graph: which faces share an edge, and what each adjacent pair looks like.

face_adjacency is the table every other function here reads: one row per edge-adjacent face pair. The rest are per-adjacency-row quantities over that table, all row-aligned with it, so a caller derives the pairs once and passes them in -- require_paired_adjacency states the one rule those keywords carry, and is public because the functions taking the pair span three modules.

face_connected_component_labels is the one whole-graph answer: face-level connected components over the same adjacency.

face_adjacency

face_adjacency(
    faces: array[int32],
    edges_sorted: Array2dInt32 | None = None,
    *,
    return_edges: Literal[False] = False,
    n_vertices: int | None = None,
) -> twt.Array2dInt32
face_adjacency(
    faces: array[int32],
    edges_sorted: Array2dInt32 | None = None,
    *,
    return_edges: Literal[True],
    n_vertices: int | None = None,
) -> tuple[twt.Array2dInt32, twt.Array2dInt32]
face_adjacency(
    faces: array[int32],
    edges_sorted: Array2dInt32 | None = None,
    *,
    return_edges: bool = False,
    n_vertices: int | None = None,
) -> (
    twt.Array2dInt32
    | tuple[twt.Array2dInt32, twt.Array2dInt32]
)

Face index pairs that share an undirected mesh edge.

Each output row lists two face indices whose triangles share an edge (vertex pair). On a closed manifold mesh every interior edge appears exactly twice in the edge list, so only edges with duplicate sorted rows are kept—boundary edges that appear once are omitted.

Parameters:

Name Type Description Default
faces array[int32]

Length-3 * n_faces wp.int32 buffer of triangle vertex indices, the same flat layout as triwarp.triangles and faces_to_edges.

required
edges_sorted Array2dInt32 | None

Optional precomputed (n_faces * 3, 2) edge rows with each row sorted so the smaller vertex index is first (as from faces_to_edges with sorted=True). When None, edges are built from faces on faces.device.

None
return_edges bool

If True, also return the shared vertex indices for each adjacency row.

False
n_vertices int | None

Optional vertex count, used as the row-hashing radix. Supplying it skips the triwarp.reduce.minmax that would otherwise infer and validate the radix, and with it a host readback that serialises the device pipeline — worth passing from loops that call this once per pass. Must be greater than every index in faces; see the warning on hash_indices_rows.

None

Returns:

Type Description
twt.Array2dInt32 or tuple of two such arrays

adjacency — shape (m, 2) on faces.device. Row k gives face indices (f0, f1) with f0 <= f1 (rows sorted in-place). Faces faces[3*f0:3*f0+3] and faces[3*f1:3*f1+3] share an edge.

When return_edges is True, also returns adjacency_edges — shape (m, 2) with the sorted vertex pair for that shared edge (one row per adjacency pair, taken from the first matching edge row).

Raises:

Type Description
RuntimeError

If faces and edges_sorted are not all on one device.

Notes

Duplicate-edge grouping uses group_int_rows with length=2, equivalent to trimesh.grouping.group_rows with require_count=2. An empty mesh yields shape (0, 2).

See Also

faces_to_edges connected_component_labels_from_edges face_connected_component_labels trimesh.graph.face_adjacency

Source code in triwarp/adjacency.py
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
def face_adjacency(
    faces: wp.array[wp.int32],
    edges_sorted: twt.Array2dInt32 | None = None,
    *,
    return_edges: bool = False,
    n_vertices: int | None = None,
) -> twt.Array2dInt32 | tuple[twt.Array2dInt32, twt.Array2dInt32]:
    """
    Face index pairs that share an undirected mesh edge.

    Each output row lists two face indices whose triangles share an edge (vertex
    pair). On a closed manifold mesh every interior edge appears exactly twice in
    the edge list, so only edges with duplicate sorted rows are kept—boundary edges
    that appear once are omitted.

    Parameters
    ----------
    faces
        Length-``3 * n_faces`` ``wp.int32`` buffer of triangle vertex indices, the
        same flat layout as [`triwarp.triangles`][triwarp.triangles] and
        [`faces_to_edges`][triwarp.edges.faces_to_edges].
    edges_sorted
        Optional precomputed ``(n_faces * 3, 2)`` edge rows with each row sorted
        so the smaller vertex index is first (as from
        [`faces_to_edges`][triwarp.edges.faces_to_edges] with
        ``sorted=True``). When ``None``, edges are built from ``faces`` on
        ``faces.device``.
    return_edges
        If ``True``, also return the shared vertex indices for each adjacency row.
    n_vertices
        Optional vertex count, used as the row-hashing radix. Supplying it skips the
        ``triwarp.reduce.minmax`` that would otherwise infer and validate the radix, and with it a
        host readback that serialises the device pipeline — worth passing from loops that call this
        once per pass. Must be greater than every index in ``faces``; see the warning on
        [`hash_indices_rows`][triwarp.grouping.hash_indices_rows].

    Returns
    -------
    twt.Array2dInt32 or tuple of two such arrays
        **adjacency** — shape ``(m, 2)`` on ``faces.device``. Row ``k`` gives face
        indices ``(f0, f1)`` with ``f0 <= f1`` (rows sorted in-place). Faces
        ``faces[3*f0:3*f0+3]`` and ``faces[3*f1:3*f1+3]`` share an edge.

        When ``return_edges`` is ``True``, also returns **adjacency_edges** —
        shape ``(m, 2)`` with the sorted vertex pair for that shared edge (one row
        per adjacency pair, taken from the first matching edge row).

    Raises
    ------
    RuntimeError
        If ``faces`` and ``edges_sorted`` are not all on one device.

    Notes
    -----
    Duplicate-edge grouping uses
    [`group_int_rows`][triwarp.grouping.group_int_rows] with
    ``length=2``, equivalent to [`trimesh.grouping.group_rows`][] with
    ``require_count=2``. An empty mesh yields shape ``(0, 2)``.

    See Also
    --------
    [`faces_to_edges`][triwarp.edges.faces_to_edges]
    [`connected_component_labels_from_edges`][triwarp.graph.connected_component_labels_from_edges]
    [`face_connected_component_labels`][triwarp.adjacency.face_connected_component_labels]
    [`trimesh.graph.face_adjacency`][]
    """
    require_same_device(faces=faces, edges_sorted=edges_sorted)
    device = faces.device
    n_faces = int(faces.shape[0]) // 3
    if n_faces == 0:
        empty_array = twt.empty_2d((0, 2), wp.int32, device=device)
        if return_edges:
            return empty_array, twt.empty_2d((0, 2), wp.int32, device=device)
        return empty_array

    if return_edges and edges_sorted is None:
        edges_sorted = tw.edges.faces_to_edges(faces, sorted=True)
    edge_groups = _edge_groups(faces, edges_sorted, n_vertices)

    # Edge ``e`` belongs to face ``e // 3``, so the owning faces need no ``edges_face`` table, no
    # gather through it, and no row sort — one kernel does the division and orders the pair.
    n_pairs = int(edge_groups.shape[0])
    adjacency = twt.empty_2d((n_pairs, 2), wp.int32, device=device)
    if n_pairs > 0:
        wp.launch(
            kernel_adjacency.edge_pairs_to_face_pairs,
            dim=n_pairs,
            inputs=[edge_groups, adjacency],
            device=device,
        )
    if return_edges:
        assert edges_sorted is not None
        if n_pairs > 0:
            # ``edge_groups[:, 0]`` is a strided column view; Warp's fancy indexing reads the
            # underlying flat buffer and ignores the stride, so materialize a contiguous index
            # first. (Slicing an empty first axis also raises, hence the guard.)
            first_edge_index = wp.clone(edge_groups[:, 0])
            adjacency_edges = tw.array.gather(edges_sorted, first_edge_index)
        else:
            adjacency_edges = twt.empty_2d((0, 2), wp.int32, device=device)
        return twt.as_array2d(adjacency, wp.int32), twt.as_array2d(adjacency_edges, wp.int32)
    return twt.as_array2d(adjacency, wp.int32)

require_paired_adjacency

require_paired_adjacency(
    face_adjacency: Array2dInt32 | None,
    face_adjacency_edges: Array2dInt32 | None,
) -> None

Raise unless a precomputed face-adjacency pair is either wholly given or wholly omitted.

The contract behind every face_adjacency= / face_adjacency_edges= keyword in the package: the two tables are row-aligned halves of one answer and are useless apart, so a function that accepts them accepts both or neither. Public because the functions that take the pair live in three modules -- here, triwarp.curvature and triwarp.validation -- and they must all reject the same call with the same message; a caller that validates its own arguments before dispatching between them can use it for that too.

Call it before an empty-mesh guard, not after. A half-supplied pair is a caller bug whatever the mesh is, and the four wrappers that take the pair once disagreed about this, so the same wrong call raised or returned an empty answer depending on the input. The derivation goes the other way round -- below the guard -- because on an empty mesh face_adjacency would build two empty tables nothing reads.

Parameters:

Name Type Description Default
face_adjacency Array2dInt32 | None

Candidate (m, 2) face pairs, or None.

required
face_adjacency_edges Array2dInt32 | None

Candidate (m, 2) shared-edge endpoints, or None.

required

Raises:

Type Description
ValueError

If exactly one of the two is given.

RuntimeError

If face_adjacency and face_adjacency_edges are not all on one device.

See Also

face_adjacency Produces the pair, with return_edges=True. face_adjacency_unshared face_adjacency_projections face_adjacency_convex

Source code in triwarp/adjacency.py
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
def require_paired_adjacency(
    face_adjacency: twt.Array2dInt32 | None, face_adjacency_edges: twt.Array2dInt32 | None
) -> None:
    """
    Raise unless a precomputed face-adjacency pair is either wholly given or wholly omitted.

    The contract behind every ``face_adjacency=`` / ``face_adjacency_edges=`` keyword in the
    package: the two tables are row-aligned halves of one answer and are useless apart, so a
    function that accepts them accepts both or neither. Public because the functions that take the
    pair live in three modules -- here, [`triwarp.curvature`][triwarp.curvature] and
    [`triwarp.validation`][triwarp.validation] -- and they must all reject the same call with the
    same message; a caller that validates its own arguments before dispatching between them can
    use it for that too.

    **Call it before an empty-mesh guard, not after.** A half-supplied pair is a caller bug
    whatever the mesh is, and the four wrappers that take the pair once disagreed about this, so
    the same wrong call raised or returned an empty answer depending on the input. The *derivation*
    goes the other way round -- below the guard -- because on an empty mesh
    [`face_adjacency`][triwarp.adjacency.face_adjacency] would build two empty tables nothing reads.

    Parameters
    ----------
    face_adjacency
        Candidate ``(m, 2)`` face pairs, or ``None``.
    face_adjacency_edges
        Candidate ``(m, 2)`` shared-edge endpoints, or ``None``.

    Raises
    ------
    ValueError
        If exactly one of the two is given.
    RuntimeError
        If ``face_adjacency`` and ``face_adjacency_edges`` are not all on one device.

    See Also
    --------
    [`face_adjacency`][triwarp.adjacency.face_adjacency]
        Produces the pair, with ``return_edges=True``.
    [`face_adjacency_unshared`][triwarp.adjacency.face_adjacency_unshared]
    [`face_adjacency_projections`][triwarp.adjacency.face_adjacency_projections]
    [`face_adjacency_convex`][triwarp.adjacency.face_adjacency_convex]
    """
    require_same_device(face_adjacency=face_adjacency, face_adjacency_edges=face_adjacency_edges)
    if (face_adjacency is None) != (face_adjacency_edges is None):
        raise ValueError(
            "face_adjacency and face_adjacency_edges must both be provided or both omitted"
        )

vertex_face_adjacency

vertex_face_adjacency(
    faces: array[int32], *, n_vertices: int | None = None
) -> tuple[wp.array[wp.int32], wp.array[wp.int32]]

Incidence CSR of the faces touching each vertex, as (vertex_faces, offsets).

Row v is vertex_faces[offsets[v] : offsets[v + 1]] and lists every face that references vertex v, once per reference. offsets has length n_vertices + 1, so its last entry is the total 3 * n_faces and no caller needs a sentinel appended. Values first and offsets second is the package's packed-buffer convention -- see array.pack_1d_arrays, which states it.

Built by counting sort rather than from halfedge twins, and that is a deliberate limitation rather than an omission: each row is a set, not a rotation. Ordering a row would require the halfedge structure, which does not exist at a vertex-non-manifold vertex — and the decimator and normal-flip guards that consume this must run on exactly such meshes. Use vertex_one_rings when the rotational order is what is wanted; it requires an edge-manifold mesh in exchange.

Parameters:

Name Type Description Default
faces array[int32]

Length-3 * n_faces wp.int32 flat triangle index buffer.

required
n_vertices int | None

Number of vertices, i.e. the number of CSR rows. When None it is inferred from faces with array.index_bound, which costs one host readback; pass it when the caller already knows it. Rows for vertices no face references come out empty.

None

Returns:

Name Type Description
vertex_faces array[int32]

Length 3 * n_faces face indices, grouped by vertex, arbitrary order within a row.

offsets array[int32]

Length n_vertices + 1 row offsets on faces.device.

See Also

face_adjacency vertex_one_rings igl.vertex_triangle_adjacency

Source code in triwarp/adjacency.py
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
def vertex_face_adjacency(
    faces: wp.array[wp.int32], *, n_vertices: int | None = None
) -> tuple[wp.array[wp.int32], wp.array[wp.int32]]:
    """
    Incidence CSR of the faces touching each vertex, as ``(vertex_faces, offsets)``.

    Row ``v`` is ``vertex_faces[offsets[v] : offsets[v + 1]]`` and lists every face that references
    vertex ``v``, once per reference. ``offsets`` has length ``n_vertices + 1``, so its last entry
    is the total ``3 * n_faces`` and no caller needs a sentinel appended. Values first and offsets
    second is the package's packed-buffer convention -- see
    [`array.pack_1d_arrays`][triwarp.array.pack_1d_arrays], which states it.

    Built by counting sort rather than from halfedge twins, and that is a deliberate limitation
    rather than an omission: **each row is a set, not a rotation**. Ordering a row would require the
    halfedge structure, which does not exist at a vertex-non-manifold vertex — and the decimator and
    normal-flip guards that consume this must run on exactly such meshes. Use
    [`vertex_one_rings`][triwarp.halfedge.vertex_one_rings] when the rotational order is what is
    wanted; it requires an edge-manifold mesh in exchange.

    Parameters
    ----------
    faces
        Length-``3 * n_faces`` ``wp.int32`` flat triangle index buffer.
    n_vertices
        Number of vertices, i.e. the number of CSR rows. When ``None`` it is inferred from
        ``faces`` with [`array.index_bound`][triwarp.array.index_bound], which costs one host
        readback; pass it when the caller already knows it. Rows for vertices no face references
        come out empty.

    Returns
    -------
    vertex_faces : wp.array[wp.int32]
        Length ``3 * n_faces`` face indices, grouped by vertex, arbitrary order within a row.
    offsets : wp.array[wp.int32]
        Length ``n_vertices + 1`` row offsets on ``faces.device``.

    See Also
    --------
    [`face_adjacency`][triwarp.adjacency.face_adjacency]
    [`vertex_one_rings`][triwarp.halfedge.vertex_one_rings]
    ``igl.vertex_triangle_adjacency``
    """
    device = faces.device
    n_faces = int(faces.shape[0]) // 3
    row_count = tw.array.index_bound(faces) if n_vertices is None else int(n_vertices)

    offsets = wp.zeros(row_count + 1, dtype=wp.int32, device=device)
    if n_faces == 0 or row_count == 0:
        # ``row_count == 0`` with faces present (reachable only via an explicit ``n_vertices=0``)
        # would otherwise hand back an unwritten ``3 * n_faces`` buffer of allocator garbage.
        return wp.zeros(3 * n_faces, dtype=wp.int32, device=device), offsets

    vertex_faces = wp.empty(3 * n_faces, dtype=wp.int32, device=device)

    counts = wp.zeros(row_count, dtype=wp.int32, device=device)
    # A flat face buffer *is* the corner -> vertex map, so one launch over all 3 * n_faces corners
    # gives each vertex its incident-face count.
    wp.launch(
        kernel_scatter.count_occurrences, dim=3 * n_faces, inputs=[faces, counts], device=device
    )
    # Deliberately NOT tw.array.counts_to_offsets: that helper always reads the total back, and
    # this function never needs it (it is 3 * n_faces, known on the host). Converting for
    # symmetry would add a device synchronization where there is currently none.
    wp.utils.array_scan(counts, out_array=offsets[1:], inclusive=True)
    cursor = wp.zeros(row_count, dtype=wp.int32, device=device)
    wp.launch(
        kernel_adjacency.scatter_vertex_faces,
        dim=n_faces,
        inputs=[faces, offsets, cursor, vertex_faces],
        device=device,
    )
    return vertex_faces, offsets

face_adjacency_unshared

face_adjacency_unshared(
    faces: array[int32],
    face_adjacency: Array2dInt32 | None = None,
    face_adjacency_edges: Array2dInt32 | None = None,
    *,
    n_vertices: int | None = None,
) -> twt.Array2dInt32

Vertex on each adjacent face that is not on their shared edge.

For each row of face_adjacency, column 0 is the unshared vertex index on the first face and column 1 on the second face. When a face does not have exactly one vertex off the shared edge (degenerate case), that entry is -1.

The answer is defined by the recorded shared edge of each adjacency row, matching trimesh.graph.face_adjacency_unshared exactly. This matters only for duplicate faces: two coincident triangles meet along all three of their edges, so they produce three adjacency rows and each one reports the corner off its own edge ([[2, 2], [1, 1], [0, 0]] for two copies of (0, 1, 2)). A cheaper "vertex of one face absent from the other" rule would return -1 for all three, and would not be trimesh.

Parameters:

Name Type Description Default
faces array[int32]

Length-3 * n_faces wp.int32 buffer of triangle vertex indices, the same flat layout as face_adjacency.

required
face_adjacency Array2dInt32 | None

Optional (m, 2) face index pairs from face_adjacency. When None, adjacency and shared edges are derived from faces directly and neither table is materialized.

None
face_adjacency_edges Array2dInt32 | None

Optional (m, 2) sorted shared vertex pairs (as from face_adjacency with return_edges=True). Must be supplied together with face_adjacency or omitted with it.

None
n_vertices int | None

Optional vertex count used as the row-hashing radix, forwarded to face_adjacency. Ignored when the adjacency tables are supplied. Skips a host readback; see that function's note.

None

Returns:

Type Description
Array2dInt32

Shape (m, 2) on faces.device. Row k gives vertex indices into faces for the corners not on face_adjacency_edges[k], or -1 when degenerate. Rows are in the same order face_adjacency returns for the same faces, so the two line up row-for-row whether or not the tables were passed in.

Raises:

Type Description
ValueError

If only one of face_adjacency and face_adjacency_edges is provided, or if their row counts differ.

RuntimeError

If faces, face_adjacency and face_adjacency_edges are not all on one device.

See Also

face_adjacency trimesh.graph.face_adjacency_unshared

Source code in triwarp/adjacency.py
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
def face_adjacency_unshared(
    faces: wp.array[wp.int32],
    face_adjacency: twt.Array2dInt32 | None = None,
    face_adjacency_edges: twt.Array2dInt32 | None = None,
    *,
    n_vertices: int | None = None,
) -> twt.Array2dInt32:
    """
    Vertex on each adjacent face that is not on their shared edge.

    For each row of ``face_adjacency``, column 0 is the unshared vertex index on
    the first face and column 1 on the second face. When a face does not have
    exactly one vertex off the shared edge (degenerate case), that entry is ``-1``.

    The answer is defined by the **recorded shared edge** of each adjacency row, matching
    [`trimesh.graph.face_adjacency_unshared`][] exactly. This matters only for duplicate faces:
    two coincident triangles meet along all three of their edges, so they produce three adjacency
    rows and each one reports the corner off *its own* edge (``[[2, 2], [1, 1], [0, 0]]`` for two
    copies of ``(0, 1, 2)``). A cheaper "vertex of one face absent from the other" rule would
    return ``-1`` for all three, and would not be trimesh.

    Parameters
    ----------
    faces
        Length-``3 * n_faces`` ``wp.int32`` buffer of triangle vertex indices, the
        same flat layout as [`face_adjacency`][triwarp.adjacency.face_adjacency].
    face_adjacency
        Optional ``(m, 2)`` face index pairs from
        [`face_adjacency`][triwarp.adjacency.face_adjacency]. When
        ``None``, adjacency and shared edges are derived from ``faces`` directly and neither table
        is materialized.
    face_adjacency_edges
        Optional ``(m, 2)`` sorted shared vertex pairs (as from
        [`face_adjacency`][triwarp.adjacency.face_adjacency] with ``return_edges=True``).
        Must be supplied
        together with ``face_adjacency`` or omitted with it.
    n_vertices
        Optional vertex count used as the row-hashing radix, forwarded to
        [`face_adjacency`][triwarp.adjacency.face_adjacency]. Ignored when the adjacency tables are
        supplied. Skips a host readback; see that function's note.

    Returns
    -------
    twt.Array2dInt32
        Shape ``(m, 2)`` on ``faces.device``. Row ``k`` gives vertex indices into
        ``faces`` for the corners not on ``face_adjacency_edges[k]``, or ``-1``
        when degenerate. Rows are in the same order
        [`face_adjacency`][triwarp.adjacency.face_adjacency] returns for the same ``faces``, so the
        two line up row-for-row whether or not the tables were passed in.

    Raises
    ------
    ValueError
        If only one of ``face_adjacency`` and ``face_adjacency_edges`` is provided,
        or if their row counts differ.
    RuntimeError
        If ``faces``, ``face_adjacency`` and ``face_adjacency_edges`` are not all on one device.

    See Also
    --------
    [`face_adjacency`][triwarp.adjacency.face_adjacency]
    [`trimesh.graph.face_adjacency_unshared`][]
    """
    require_same_device(
        faces=faces, face_adjacency=face_adjacency, face_adjacency_edges=face_adjacency_edges
    )
    # The derive branch below deliberately does *not* call face_adjacency, recovering both
    # owning faces and the shared edge from the grouped edge indices instead. Only the pairing
    # rule is shared with the other wrappers that take this pair.
    require_paired_adjacency(face_adjacency, face_adjacency_edges)
    device = faces.device
    # Both branches end in one launch over ``m`` adjacency rows writing one ``(m, 2)`` buffer, so
    # the two differ only in the kernel and the table it reads. Resolving that first, and letting a
    # single ``m == 0`` return cover an empty mesh, an empty adjacency and an empty supplied table
    # alike, is what keeps the empty case to *one* allocation: deriving used to build an empty
    # ``edge_groups`` only to size an empty output off it.
    if face_adjacency is None:
        edge_groups = _edge_groups(faces, None, n_vertices) if int(faces.shape[0]) >= 3 else None
        m = 0 if edge_groups is None else int(edge_groups.shape[0])
        kernel, tables = kernel_adjacency.face_adjacency_unshared_from_edges, (edge_groups,)
    else:
        assert face_adjacency_edges is not None
        if face_adjacency.shape[0] != face_adjacency_edges.shape[0]:
            raise ValueError(
                "face_adjacency and face_adjacency_edges row counts must match, "
                f"got {face_adjacency.shape[0]} and {face_adjacency_edges.shape[0]}"
            )
        m = int(face_adjacency.shape[0])
        kernel, tables = (
            kernel_adjacency.face_adjacency_unshared,
            (face_adjacency, face_adjacency_edges),
        )

    unshared = twt.empty_2d((m, 2), wp.int32, device=device)
    if m > 0:
        wp.launch(kernel, dim=m, inputs=[faces, *tables, unshared], device=device)
    return twt.as_array2d(unshared, wp.int32)

face_adjacency_angles

face_adjacency_angles(
    vertices: array[vec3],
    faces: array[int32],
    face_adjacency: Array2dInt32 | None = None,
    face_normals: array[vec3] | None = None,
) -> wp.array[wp.float32]

Unsigned angle in radians between each pair of adjacent faces.

For each row of face_adjacency, the angle is computed from the two corresponding face normals (unit vectors). Pair it with face_adjacency_convex for the sign: that function reports which side of each shared edge the pair folds towards, which is exactly the sign this unsigned magnitude is missing.

Parameters:

Name Type Description Default
vertices array[vec3]

(n_vertices,) mesh vertex positions on the target device.

required
faces array[int32]

Length-3 * n_faces flat triangle index buffer (same layout as face_adjacency).

required
face_adjacency Array2dInt32 | None

Optional (m, 2) face index pairs from face_adjacency. When None, adjacency is computed from faces.

None
face_normals array[vec3] | None

Optional length-n_faces unit face normals. When None, normals are computed from vertices and faces via face_normals_and_areas.

None

Returns:

Type Description
array[float32]

Length m unsigned angles in radians on faces.device, one per face_adjacency row. Empty when there are no faces or no adjacency pairs.

Raises:

Type Description
RuntimeError

If vertices, faces, face_adjacency and face_normals are not all on one device.

See Also

face_adjacency face_adjacency_convex The sign this magnitude omits: convex or concave, per adjacency row. vector_angle trimesh.Trimesh.face_adjacency_angles

Source code in triwarp/adjacency.py
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
def face_adjacency_angles(
    vertices: wp.array[wp.vec3],
    faces: wp.array[wp.int32],
    face_adjacency: twt.Array2dInt32 | None = None,
    face_normals: wp.array[wp.vec3] | None = None,
) -> wp.array[wp.float32]:
    """
    Unsigned angle in radians between each pair of adjacent faces.

    For each row of ``face_adjacency``, the angle is computed from the two
    corresponding face normals (unit vectors). Pair it with
    [`face_adjacency_convex`][triwarp.adjacency.face_adjacency_convex] for the sign: that function
    reports which side of each shared edge the pair folds towards, which is exactly the sign this
    unsigned magnitude is missing.

    Parameters
    ----------
    vertices
        ``(n_vertices,)`` mesh vertex positions on the target device.
    faces
        Length-``3 * n_faces`` flat triangle index buffer (same layout as
        [`face_adjacency`][triwarp.adjacency.face_adjacency]).
    face_adjacency
        Optional ``(m, 2)`` face index pairs from
        [`face_adjacency`][triwarp.adjacency.face_adjacency]. When
        ``None``, adjacency is computed from ``faces``.
    face_normals
        Optional length-``n_faces`` unit face normals. When ``None``, normals
        are computed from ``vertices`` and ``faces`` via
        [`face_normals_and_areas`][triwarp.triangles.face_normals_and_areas].

    Returns
    -------
    wp.array[wp.float32]
        Length ``m`` unsigned angles in radians on ``faces.device``, one per
        ``face_adjacency`` row. Empty when there are no faces or no adjacency pairs.

    Raises
    ------
    RuntimeError
        If ``vertices``, ``faces``, ``face_adjacency`` and ``face_normals`` are not all on one
        device.

    See Also
    --------
    [`face_adjacency`][triwarp.adjacency.face_adjacency]
    [`face_adjacency_convex`][triwarp.adjacency.face_adjacency_convex]
        The sign this magnitude omits: convex or concave, per adjacency row.
    [`vector_angle`][triwarp.points.vector_angle]
    [`trimesh.Trimesh.face_adjacency_angles`][]
    """
    require_same_device(
        vertices=vertices, faces=faces, face_adjacency=face_adjacency, face_normals=face_normals
    )
    device = faces.device
    n_faces = int(faces.shape[0]) // 3
    if n_faces == 0:
        return wp.empty(0, dtype=wp.float32, device=device)

    if face_adjacency is None:
        face_adjacency = tw.adjacency.face_adjacency(faces, n_vertices=int(vertices.shape[0]))
    if face_normals is None:
        face_normals, _ = tw.triangles.face_normals_and_areas(vertices, faces)

    m = int(face_adjacency.shape[0])
    if m == 0:
        return wp.empty(0, dtype=wp.float32, device=device)

    out_angles = wp.empty(m, dtype=wp.float32, device=device)
    wp.launch(
        kernel_adjacency.face_adjacency_angles,
        dim=m,
        inputs=[face_normals, face_adjacency, out_angles],
        device=device,
    )
    return out_angles

face_adjacency_projections

face_adjacency_projections(
    vertices: array[vec3],
    faces: array[int32],
    face_adjacency: Array2dInt32 | None = None,
    face_adjacency_edges: Array2dInt32 | None = None,
    face_adjacency_unshared: Array2dInt32 | None = None,
    face_normals: array[vec3] | None = None,
) -> wp.array[wp.float32]

Project each adjacent face pair's non-shared vertex onto the first face plane.

For each row of face_adjacency, the dot product is taken between the normal of face face_adjacency[k, 0] and the vector from one endpoint of the shared edge to the unshared vertex on face_adjacency[k, 1].

Parameters:

Name Type Description Default
vertices array[vec3]

(n_vertices,) mesh vertex positions on the target device.

required
faces array[int32]

Length-3 * n_faces flat triangle index buffer (same layout as face_adjacency).

required
face_adjacency Array2dInt32 | None

Optional (m, 2) face index pairs from face_adjacency. When None, adjacency and shared edges are computed from faces.

None
face_adjacency_edges Array2dInt32 | None

Optional (m, 2) sorted shared vertex pairs (as from face_adjacency with return_edges=True). Must be supplied together with face_adjacency or omitted with it.

None
face_adjacency_unshared Array2dInt32 | None

Optional (m, 2) unshared vertex indices per face pair from face_adjacency_unshared. When None, computed from faces and the adjacency data.

None
face_normals array[vec3] | None

Optional length-n_faces unit face normals. When None, normals are computed from vertices and faces via face_normals_and_areas.

None

Returns:

Type Description
array[float32]

Length m projections on faces.device, one per face_adjacency row. Empty when there are no faces or no adjacency pairs. A row whose second face is degenerate (its face_adjacency_unshared entry is -1) reads as +inf, so it never registers as convex.

Raises:

Type Description
ValueError

If only one of face_adjacency and face_adjacency_edges is provided, or if a supplied face_adjacency_unshared has a different row count from face_adjacency.

RuntimeError

If vertices, faces, face_adjacency, face_adjacency_edges, face_adjacency_unshared and face_normals are not all on one device.

See Also

face_adjacency_convex trimesh.Trimesh.face_adjacency_projections

Source code in triwarp/adjacency.py
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
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
576
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
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
def face_adjacency_projections(
    vertices: wp.array[wp.vec3],
    faces: wp.array[wp.int32],
    face_adjacency: twt.Array2dInt32 | None = None,
    face_adjacency_edges: twt.Array2dInt32 | None = None,
    face_adjacency_unshared: twt.Array2dInt32 | None = None,
    face_normals: wp.array[wp.vec3] | None = None,
) -> wp.array[wp.float32]:
    """
    Project each adjacent face pair's non-shared vertex onto the first face plane.

    For each row of ``face_adjacency``, the dot product is taken between the
    normal of face ``face_adjacency[k, 0]`` and the vector from one endpoint of
    the shared edge to the unshared vertex on ``face_adjacency[k, 1]``.

    Parameters
    ----------
    vertices
        ``(n_vertices,)`` mesh vertex positions on the target device.
    faces
        Length-``3 * n_faces`` flat triangle index buffer (same layout as
        [`face_adjacency`][triwarp.adjacency.face_adjacency]).
    face_adjacency
        Optional ``(m, 2)`` face index pairs from
        [`face_adjacency`][triwarp.adjacency.face_adjacency]. When ``None``, adjacency and
        shared edges are computed from ``faces``.
    face_adjacency_edges
        Optional ``(m, 2)`` sorted shared vertex pairs (as from
        [`face_adjacency`][triwarp.adjacency.face_adjacency] with ``return_edges=True``).
        Must be supplied together with ``face_adjacency`` or omitted with it.
    face_adjacency_unshared
        Optional ``(m, 2)`` unshared vertex indices per face pair from
        [`face_adjacency_unshared`][triwarp.adjacency.face_adjacency_unshared]. When ``None``,
        computed from ``faces`` and the adjacency data.
    face_normals
        Optional length-``n_faces`` unit face normals. When ``None``, normals
        are computed from ``vertices`` and ``faces`` via
        [`face_normals_and_areas`][triwarp.triangles.face_normals_and_areas].

    Returns
    -------
    wp.array[wp.float32]
        Length ``m`` projections on ``faces.device``, one per ``face_adjacency``
        row. Empty when there are no faces or no adjacency pairs. A row whose second face is
        degenerate (its
        [`face_adjacency_unshared`][triwarp.adjacency.face_adjacency_unshared] entry is ``-1``)
        reads as ``+inf``, so it never registers as convex.

    Raises
    ------
    ValueError
        If only one of ``face_adjacency`` and ``face_adjacency_edges`` is provided, or if a
        supplied ``face_adjacency_unshared`` has a different row count from ``face_adjacency``.
    RuntimeError
        If ``vertices``, ``faces``, ``face_adjacency``, ``face_adjacency_edges``,
        ``face_adjacency_unshared`` and ``face_normals`` are not all on one device.

    See Also
    --------
    [`face_adjacency_convex`][triwarp.adjacency.face_adjacency_convex]
    [`trimesh.Trimesh.face_adjacency_projections`][]
    """
    require_same_device(
        vertices=vertices,
        faces=faces,
        face_adjacency=face_adjacency,
        face_adjacency_edges=face_adjacency_edges,
        face_adjacency_unshared=face_adjacency_unshared,
        face_normals=face_normals,
    )
    device = faces.device
    n_faces = int(faces.shape[0]) // 3
    # The pairing check runs *before* the empty-mesh guard, so a caller who passed only one half
    # of the pair is told about it whatever the mesh is -- the four wrappers that take this pair
    # used to disagree about that, two validating first and two returning empty first. The
    # *resolve* stays below the guard, because on an empty mesh it would allocate two empty tables
    # nothing reads.
    require_paired_adjacency(face_adjacency, face_adjacency_edges)
    if n_faces == 0:
        return wp.empty(0, dtype=wp.float32, device=device)

    if face_adjacency is None:
        # ``tw.adjacency.`` rather than a bare call: the parameter shadows the module-level
        # ``face_adjacency`` it derives from.
        face_adjacency, face_adjacency_edges = tw.adjacency.face_adjacency(
            faces, return_edges=True, n_vertices=int(vertices.shape[0])
        )
    assert face_adjacency_edges is not None
    m = int(face_adjacency.shape[0])
    if m == 0:
        return wp.empty(0, dtype=wp.float32, device=device)

    if face_adjacency_unshared is None:
        face_adjacency_unshared = tw.adjacency.face_adjacency_unshared(
            faces, face_adjacency=face_adjacency, face_adjacency_edges=face_adjacency_edges
        )
    elif int(face_adjacency_unshared.shape[0]) != m:
        # A caller-supplied table is otherwise trusted as-is; the kernel below indexes it at every
        # row up to ``m``, so a shorter table is an out-of-bounds read rather than a wrong answer.
        raise ValueError(
            "face_adjacency_unshared row count must match face_adjacency, got "
            f"{face_adjacency_unshared.shape[0]} and {m}."
        )
    if face_normals is None:
        face_normals, _ = tw.triangles.face_normals_and_areas(vertices, faces)

    out_projections = wp.empty(m, dtype=wp.float32, device=device)
    wp.launch(
        kernel_adjacency.face_adjacency_projections,
        dim=m,
        inputs=[
            vertices,
            face_normals,
            face_adjacency,
            face_adjacency_edges,
            face_adjacency_unshared,
            out_projections,
        ],
        device=device,
    )
    return out_projections

face_adjacency_convex

face_adjacency_convex(
    vertices: array[vec3],
    faces: array[int32],
    face_adjacency: Array2dInt32 | None = None,
    face_adjacency_edges: Array2dInt32 | None = None,
    face_adjacency_unshared: Array2dInt32 | None = None,
    face_normals: array[vec3] | None = None,
) -> wp.array[wp.bool]

Return face pairs that are adjacent and locally convex.

A pair is locally convex when the unshared vertex of the second face, projected onto the plane of the first face, has a projection less than TOLERANCE_MERGE.

Parameters:

Name Type Description Default
vertices array[vec3]

(n_vertices,) mesh vertex positions on the target device.

required
faces array[int32]

Length-3 * n_faces flat triangle index buffer (same layout as face_adjacency).

required
face_adjacency Array2dInt32 | None

Optional (m, 2) face index pairs from face_adjacency. When None, adjacency and shared edges are computed from faces.

None
face_adjacency_edges Array2dInt32 | None

Optional (m, 2) sorted shared vertex pairs. Must be supplied together with face_adjacency or omitted with it.

None
face_adjacency_unshared Array2dInt32 | None

Optional (m, 2) unshared vertex indices per face pair.

None
face_normals array[vec3] | None

Optional length-n_faces unit face normals.

None

Returns:

Type Description
array[bool]

Length m boolean mask on faces.device, one per face_adjacency row. Empty when there are no faces or no adjacency pairs.

Raises:

Type Description
ValueError

If only one of face_adjacency and face_adjacency_edges is provided, or if a supplied face_adjacency_unshared has a different row count from face_adjacency (raised by face_adjacency_projections, which this function delegates to).

RuntimeError

If vertices, faces, face_adjacency, face_adjacency_edges, face_adjacency_unshared and face_normals are not all on one device.

See Also

face_adjacency_projections trimesh.Trimesh.face_adjacency_convex

Source code in triwarp/adjacency.py
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
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
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
def face_adjacency_convex(
    vertices: wp.array[wp.vec3],
    faces: wp.array[wp.int32],
    face_adjacency: twt.Array2dInt32 | None = None,
    face_adjacency_edges: twt.Array2dInt32 | None = None,
    face_adjacency_unshared: twt.Array2dInt32 | None = None,
    face_normals: wp.array[wp.vec3] | None = None,
) -> wp.array[wp.bool]:
    """
    Return face pairs that are adjacent and locally convex.

    A pair is locally convex when the unshared vertex of the second face,
    projected onto the plane of the first face, has a projection less than
    [`TOLERANCE_MERGE`][triwarp.constants.TOLERANCE_MERGE].

    Parameters
    ----------
    vertices
        ``(n_vertices,)`` mesh vertex positions on the target device.
    faces
        Length-``3 * n_faces`` flat triangle index buffer (same layout as
        [`face_adjacency`][triwarp.adjacency.face_adjacency]).
    face_adjacency
        Optional ``(m, 2)`` face index pairs from
        [`face_adjacency`][triwarp.adjacency.face_adjacency]. When ``None``, adjacency and
        shared edges are computed from ``faces``.
    face_adjacency_edges
        Optional ``(m, 2)`` sorted shared vertex pairs. Must be supplied
        together with ``face_adjacency`` or omitted with it.
    face_adjacency_unshared
        Optional ``(m, 2)`` unshared vertex indices per face pair.
    face_normals
        Optional length-``n_faces`` unit face normals.

    Returns
    -------
    wp.array[wp.bool]
        Length ``m`` boolean mask on ``faces.device``, one per
        ``face_adjacency`` row. Empty when there are no faces or no adjacency
        pairs.

    Raises
    ------
    ValueError
        If only one of ``face_adjacency`` and ``face_adjacency_edges`` is provided, or if a
        supplied ``face_adjacency_unshared`` has a different row count from ``face_adjacency``
        (raised by [`face_adjacency_projections`][triwarp.adjacency.face_adjacency_projections],
        which this function delegates to).
    RuntimeError
        If ``vertices``, ``faces``, ``face_adjacency``, ``face_adjacency_edges``,
        ``face_adjacency_unshared`` and ``face_normals`` are not all on one device.

    See Also
    --------
    [`face_adjacency_projections`][triwarp.adjacency.face_adjacency_projections]
    [`trimesh.Trimesh.face_adjacency_convex`][]
    """
    require_same_device(
        vertices=vertices,
        faces=faces,
        face_adjacency=face_adjacency,
        face_adjacency_edges=face_adjacency_edges,
        face_adjacency_unshared=face_adjacency_unshared,
        face_normals=face_normals,
    )
    device = faces.device
    n_faces = int(faces.shape[0]) // 3
    require_paired_adjacency(face_adjacency, face_adjacency_edges)
    if n_faces == 0:
        return wp.empty(0, dtype=wp.bool, device=device)

    if face_adjacency is None:
        # ``tw.adjacency.`` rather than a bare call: the parameter shadows the module-level
        # ``face_adjacency`` it derives from.
        face_adjacency, face_adjacency_edges = tw.adjacency.face_adjacency(
            faces, return_edges=True, n_vertices=int(vertices.shape[0])
        )
    assert face_adjacency_edges is not None

    m = int(face_adjacency.shape[0])
    if m == 0:
        return wp.empty(0, dtype=wp.bool, device=device)

    projections = face_adjacency_projections(
        vertices,
        faces,
        face_adjacency=face_adjacency,
        face_adjacency_edges=face_adjacency_edges,
        face_adjacency_unshared=face_adjacency_unshared,
        face_normals=face_normals,
    )
    out_convex = wp.empty(m, dtype=wp.bool, device=device)
    wp.map(kernel_array.less, projections, TOLERANCE_MERGE_CONSTANT, out=out_convex)
    return out_convex

face_connected_component_labels

face_connected_component_labels(
    faces: array[int32],
) -> wp.array[wp.int32]

Connected-component label per face (face-adjacency graph).

The mesh entry point to the one labelling engine: this builds face_adjacency and hands it to connected_component_labels_from_edges with node_count = n_faces. There is no second implementation -- graph's is mesh-agnostic and is what every caller in the package bottoms out in, including Trimesh.face_connected_component_labels, which calls it directly to reuse its own cached adjacency instead of rebuilding one here.

Parameters:

Name Type Description Default
faces array[int32]

Length-3 * n_faces flat triangle index buffer (same as face_adjacency).

required

Returns:

Type Description
array[int32]

Length n_faces on faces.device.

See Also

connected_component_labels connected_component_labels_from_edges face_adjacency

Source code in triwarp/adjacency.py
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
def face_connected_component_labels(faces: wp.array[wp.int32]) -> wp.array[wp.int32]:
    """
    Connected-component label per face (face-adjacency graph).

    The mesh entry point to the one labelling engine: this builds
    [`face_adjacency`][triwarp.adjacency.face_adjacency] and hands it to
    [`connected_component_labels_from_edges`][triwarp.graph.connected_component_labels_from_edges]
    with ``node_count = n_faces``. There is no second implementation -- ``graph``'s is
    mesh-agnostic and is what every caller in the package bottoms out in, including
    [`Trimesh.face_connected_component_labels`][triwarp.mesh.Trimesh], which calls it directly to
    reuse its own cached adjacency instead of rebuilding one here.

    Parameters
    ----------
    faces
        Length-``3 * n_faces`` flat triangle index buffer (same as
        [`face_adjacency`][triwarp.adjacency.face_adjacency]).

    Returns
    -------
    wp.array[wp.int32]
        Length ``n_faces`` on ``faces.device``.

    See Also
    --------
    [`connected_component_labels`][triwarp.graph.connected_component_labels]
    [`connected_component_labels_from_edges`][triwarp.graph.connected_component_labels_from_edges]
    [`face_adjacency`][triwarp.adjacency.face_adjacency]
    """
    n_faces = int(faces.shape[0]) // 3
    adjacency = face_adjacency(faces)
    return tw.graph.connected_component_labels_from_edges(
        adjacency, node_count=n_faces, validate=False
    )