QuadMesh topology with ghost vertices/quads - use when working with QuadMesh, QuadTopology, mesh filters, vertex rings, or any code in crates/shine-game/src/math/mesh/. Covers navigation, boundary detection, and common mesh operation patterns.
Quick reference for working with QuadTopology in shine-services.
QuadTopology uses ghost vertices and ghost quads to close the mesh topology:
VertIdx(vertex_count) - a topological vertex with no positionThis eliminates special-case boundary handling in mesh operations.
Ghost quad structure: For boundary edges (v0→v1→v2), ghost quad is [ghost, v2, v1, v0] - forms a pyramid with ghost vertex as apex, boundary vertices reversed to match neighbor adjacency across the boundary edge.
QuadTopology::new(
vertex_count: usize,
polygon: Vec<VertIdx>, // Boundary vertices in order (must be even length)
quads: Vec<[VertIdx; 4]>, // Real quads (CCW winding)
) -> Result<Self, QuadTopologyError>
Validates inputs, generates N/2 ghost quads for N boundary vertices, builds neighbor adjacency and vertex→quad maps. Returns error if boundary is odd length, vertices out of range, or topology is invalid.
QuadMesh vs QuadTopology: QuadMesh bundles positions: IdxVec<VertIdx, Vec2> + topology: QuadTopology. Use QuadTopology methods for connectivity queries, QuadMesh for position-dependent operations. Ghost vertex has no position in the positions array.
struct QuadVertex { quad: QuadIdx, local: u8 } // Vertex position within a quad
struct QuadEdge { quad: QuadIdx, edge: u8 } // Edge of a quad
QuadVertex navigation (pure index arithmetic):
qv.next() → next vertex CCW around quadqv.prev() → previous vertex CCWqv.opposite() → vertex across the quadqv.outgoing_edge() → edge leaving this vertexqv.incoming_edge() → edge entering this vertexQuadEdge navigation:
qe.start() → QuadVertex at edge startqe.end() → QuadVertex at edge endTopology queries (require &QuadTopology):
topology.vertex_index(qv) → actual VertIdxtopology.edge_vertices(qe) → (VertIdx, VertIdx)topology.quad_neighbor(qe) → neighboring QuadEdgeGet all quads around a vertex (works for interior, boundary, and ghost vertices):
for qv in topology.vertex_ring(vi) {
let this_vertex = topology.vertex_index(qv);
let next_vertex = topology.vertex_index(qv.next());
// Process quad...
}
The ring always closes - ghost quads complete boundary vertices' rings.
How it works: Start at vertex_quad[vi], yield current quad, move to incoming_edge() neighbor (the edge ending at this vertex connects to previous quad in CCW order), repeat until back to start. This traverses CCW around the vertex.
topology.is_boundary_vertex(vi) // Vertex in a ghost quad
topology.is_ghost_quad(qi) // Quad contains ghost vertex
topology.edge_type(a, b) // Interior | Boundary | NotAnEdge
When iterating vertices:
for vi in topology.vertex_indices() {
// Only real vertices, ghost vertex excluded
}
When iterating quads:
for qi in topology.quad_indices() {
// Only real quads, ghost quads excluded
}
for qi in topology.ghost_quad_indices() {
// Only ghost quads
}
When processing neighbors:
for qv in topology.vertex_ring(vi) {
let neighbor = topology.vertex_index(qv.next());
// Ghost vertex has no position - skip it
if let Some(idx) = neighbor.try_into_index() {
let pos = positions[idx];
// Use position...
}
}
Pattern: try_into_index() returns None for ghost vertex, naturally filtering it.
let mut sum = Vec2::ZERO;
let mut count = 0;
for qv in topology.vertex_ring(vi) {
let neighbor = topology.vertex_index(qv.next());
if let Some(idx) = neighbor.try_into_index() {
sum += positions[idx];
count += 1;
}
}
let avg = if count > 0 { sum / count as f32 } else { positions[vi] };
for edge_idx in 0..4 {
let qe = QuadEdge { quad: qi, edge: edge_idx };
let (v0, v1) = topology.edge_vertices(qe);
let neighbor = topology.quad_neighbor(qe);
// Process edge...
}
for vi in topology.boundary_vertices() {
// Iterate boundary in order (traverses ghost vertex ring)
}
When writing mesh filters (LaplacianSmoother, QuadRelax, etc.):
Skip boundary vertices in position updates:
for vi in topology.vertex_indices() {
if topology.is_boundary_vertex(vi) {
continue; // Don't move boundary
}
// Update position...
}
Filter ghost neighbors when averaging:
let neighbor = topology.vertex_index(qv.next());
if let Some(idx) = neighbor.try_into_index() {
// Real vertex - use it
}
Iterate only real quads when checking quality:
for qi in topology.quad_indices() {
// topology.quad_indices() already filters ghosts
}
QuadTopologyError::OddBoundary(len)
QuadTopologyError::BoundaryVertexOutOfRange { vertex, vertex_count }
QuadTopologyError::DuplicateBoundaryVertex(idx)
QuadTopologyError::QuadVertexOutOfRange { vertex, vertex_count }
QuadTopologyError::QuadReferencesGhost(idx)
QuadTopologyError::IncompleteTopology { quad, edge, vertices }
topology.ghost_vertex(), not by positionpositions only has vertex_count entriesincoming_edge() neighborcrates/shine-game/src/math/mesh/quad_topology.rscrates/shine-game/src/math/mesh/quad_mesh.rscrates/shine-game/src/math/mesh/filter/*.rsIf topology seems broken:
polygon.len() % 2 == 0)topology.ghost_quad_count() == boundary.len() / 2QuadIdx::NONE in quad_neighborsvertex_ring(vi).count() > 0 for all vertices