PR #3194 — Unified Soft-Mesh Adjacency

One host class + one kernel-facing struct replace two parallel reps (dict-based MeshAdjacency and the VBD solver's ParticleForceElementAdjacencyInfo), joined by a single host→device boundary.

MeshAdjacency — host class (NumPy, single source of truth)

ArrayShapeMeaning
edge_indices[E,4][o0,o1,v0,v1]: edge endpoints + opposite verts (-1 = boundary)
edge_tri_indices[E,2][f0,f1]: the two triangles on each edge
tri_edge_indices[T,3]each triangle's 3 edges (-1 if unregistered)
v_adj_{edges,tris,springs,tets} (+_offsets)CSRper-vertex adjacency; built on demand

MeshAdjacencyDeviceData — @wp.struct (kernel-facing)

Built by to(); pure data. The v_adj_* CSR (read by VBD kernels via the get_vertex_* accessors) + edge_tri_indices/tri_edge_indices (wp.array2d, future-proofing).

Data flow

  1. Builder accumulates flat edge_indices/tri_indices.
  2. finalize() builds it once: model.soft_mesh_adjacency = MeshAdjacency(tri_indices, edge_indices).
  3. Consumers share it — VBD: init_vertex_adjacency (memoized) → .to(device) → kernels; collision: host copy via _as_numpy; Style3D: builds its own.

Why