Graphs
Plexus provides a flexible representation of meshes as a half-edge graph via the graph
module and MeshGraph
type. Graphs can store arbitrary data associated with any topological structure. Unlike iterator expressions and buffers, graphs provide efficient traversals and complex manipulation of meshes.
Note
Plexus refers to half-edges as arcs. This borrows from graph theory, where arc typically refers to a directed adjacency.
MeshGraph
s can be created in various ways, including from raw buffers, iterator expressions, incremental builders, and encodings.
Representation¶
A MeshGraph
is composed of four basic entities: vertices, arcs, edges, and faces. The figure below summarizes the connectivity in a MeshGraph
.
Arcs are directed and connect vertices. An arc that is directed toward a vertex A is an incoming arc with respect to A. Similarly, an arc directed away from such a vertex is an outgoing arc. Every vertex is associated with exactly one leading arc, which is always an outgoing arc. The vertex toward which an arc is directed is the arc's destination vertex and the other is its source vertex.
Every arc is paired with and connected to an opposite arc with an opposing direction. Given an arc from a vertex A to a vertex B, that arc will have an opposite arc from B to A. Such arcs are typically notated \overrightarrow{AB} and \overrightarrow{BA}. Together, these arcs form an edge, which is not directed. An edge and its two arcs are together called a composite edge.
Arcs are connected to adjacent arcs, known as next and previous arcs. A traversal along a series of arcs is a path. The path formed by traversing from an arc to its next arc and so on is a ring. When a face is present within a ring, the arcs will refer to that face and the face will refer to exactly one of the arcs in the ring (its leading arc). An arc with no associated face is known as a boundary arc. If either of an edge's arcs is a boundary arc, then that edge is a boundary edge.
A path is closed if it forms a loop and is open if it terminates. Rings implicitly form a loop and are therefore always closed. Paths may be notated using sequence or set notation and both forms are used to describe rings and faces.
Sequence notation is formed from the ordered sequence of vertices that a path traverses, including the source vertex of the first arc and the destination vertex of the last arc. Set notation is similar, but is implicitly closed and only includes the ordered and unique set of vertices traversed by the path. An open path over vertices A, B, and C is notated as a sequence \overrightarrow{(A,B,C)}. A closed path over vertices A, B, and C includes the arc \overrightarrow{CA} and is notated as a sequence \overrightarrow{(A,B,C,A)} or a set \overrightarrow{\{A,B,C\}}. This notation may also be used to notate arcs, but arcs typically use the shorthand notation shown above.
Together with vertices and faces, the connectivity of arcs allows for efficient traversals of topology. For example, it becomes trivial to find adjacent topologies, such as the faces that share a given vertex or the adjacent faces of a given face.
Warning
The MeshGraph
data structure has some limitations. With few exceptions, only orientable compact manifolds can be represented. Unorientable manifolds such as Möbius strips and non-manifold structures such as singularities and edge-fans cannot be modeled using MeshGraph
.
MeshGraph
s store entities associatively and mesh data is accessed using keys into storage. Keys are exposed as strongly typed and opaque values, which can be used to refer to an entity.
Data and Geometry¶
MeshGraph
accepts a single type parameter that determines the types of data associated with entities (vertices, arcs, edges, and faces) in the graph. This type must implement the GraphData
trait, which provides an associated type for each entity.
Note
Most examples on this page use the R64
type from the [decorum
][] crate and the Point2
and Point3
types from the [nalgebra
][] crate for graph data. When the geometry-nalgebra
feature is enabled, these types implement GraphData
.
The associated types specified by a GraphData
implementation determine the type exposed by the get
and get_mut
functions of views. MeshGraph
is agnostic to its data and geometry and any types satisfying the bounds on GraphData
's associated types (namely Copy
and Default
) may be used, including the unit type ()
when no data is required. However, if the Vertex
type exposes some notion of positional data via the AsPosition
trait, then geometric features become available MeshGraph
APIs. Read more about geometric traits and spatial operations here.
Views¶
MeshGraph
s expose views over their entities (vertices, arcs, edges, and faces). Views are obtained using keys or iteration and provide the primary API for interacting with a MeshGraph
's topology and data. A view is a kind of "smart pointer" to an entity in a graph. There are three types of views summarized below:
Type | Traversal | Exclusive | Geometry | Topology |
---|---|---|---|---|
Immutable | Yes | No | Immutable | Immutable |
Mutable | Yes | Yes | Mutable | Mutable |
Orphan | No | No | Mutable | N/A |
Immutable and mutable views behave similarly to Rust's &
and &mut
references: immutable views cannot mutate a graph and are not exclusive while mutable views may mutate both the data and topology of a graph but are exclusive. This example uses a view to traverse a graph:
Orphan views are similar to mutable views in that they may mutate the data of a graph, but they do not have access to the topology of a graph. Because they do not know about other vertices, arcs, etc., orphan views cannot traverse a graph in any way. These views are most useful for modifying the data and geometry of a graph and, unlike mutable views, are not exclusive. As such, iterators over topological structures in a graph sometimes emit orphan views.
Immutable and mutable views are both represented by view types, such as FaceView
. Orphan views are represented by orphan view types, such as FaceOrphan
.
Views provide access to data in a graph via get
and get_mut
methods. These methods return references to the data described by the GraphData
trait.
Interior Reborrows¶
Views associate a key with a reference to storage in order to expose an entity. Because views maintain these references internally and behave like Rust references, they are typically manipulated by value rather than by reference (for example, in function parameters).
To borrow views from another view, an interior reborrow is used, which reborrows a view's internal reference to storage containing a graph's entities. Immutable reborrows can be performed explicitly using the conversions described below:
Function | Receiver | Borrow | Output |
---|---|---|---|
to_ref | &self | &_ | Immutable |
into_ref | self | &*_ | Immutable |
It is not possible to explicitly perform a mutable interior reborrow. Such a reborrow could invalidate the originating view by performing topological mutations. Mutable reborrows are performed beneath safe APIs, such as those exposing iterators over orphan views that cannot perform topological mutations.
Warning
The into_ref
conversion is analogous to an immutable reborrow of a mutable &mut
Rust reference. Importantly, the mutable source reference remains despite the reborrow and so it is not possible to obtain an additional mutable view after using into_ref
until the originating view is dropped.
Rebinding¶
Views pair a key with a reference to the underlying storage of a graph. A view binds a key to some storage. Given a view, it is possible to rebind the view to construct a new view using the same underlying storage. It is even possible to rebind into different entities, such as rebinding a FaceView
into a VertexView
.
For example, rebinding can be used for fallible traversals that maintain mutability. A mutable view can be used to look up a key and, if such a key is found, be rebound into that topology.
Rebinding can also be useful for code that only operates on views without access to the associated MeshGraph
, because it allows arbitrary access to the graph's structure so long as the appropriate keys are available.
Traversals¶
Views can be used to traverse and search a graph's structure. Traversals are an important feature of graphs that are not supported by iterator expressions nor buffers. Most traversals involve some notion of adjacency, but MeshGraph
also provides categorical traversals.
One-to-One¶
Conversions and accessors provide one-to-one traversals from one entity to another. Conversions consume a view and emit another view while accessors borrow a view and use that borrow to produce another view. Accessors only expose immutable views with a limited lifetime while conversions expose views with the same lifetime and mutability as the source view.
Conversions and accessors are distinguished using standard Rust naming conventions. For example, into_outgoing_arc
is consuming and outgoing_arc
is borrowing; both traverse from a vertex to its outgoing arc.
Note that accessors are just sugar for performing an interior reborrow before using a conversion. Accessors allow for more fluent sequences of function calls without the need to insert repeated to_ref
calls.
Note
It is not possible to perform topological mutations using a view obtained via a borrowing traversal, because these views are always immutable. Conversions and interior reborrows must be used instead.
For some entities with a notion of adjacency, it is possible to query the shortest path between two such entities, such as vertices.
It's also possible to use a custom metric. For example, rather than a logical distance between vertices, the Euclidean distance between vertices can be used.
One-to-Many¶
A circulator is a type of iterator that provides a one-to-many traversal that examines immediately adjacent entities. For example, the face circulator of a vertex yields all faces that share that vertex, in order.
Note
Circlators only expose immediately adjacent entities and do not traverse the entire graph. Use search traversals to examine all entities in a topologically connected group.
Circulators generally begin iteration from a leading arc and then traverse topology in a deterministic order from that arc. Because mutability requires orphan views, only the data of adjacent entities can be mutated using circulators.
Search traversals visit all connected entities with some notion of adjacency. Both vertices and faces can be traversed in this way to perform searches using breadth- and depth-first ordering.
Warning
It is possible for vertices and faces to be disjoint, meaning that they do not share a path with all other vertices or faces. Therefore, these traversals are only exhaustive with respect to the topologically connected group with which the initiating view is associated; they do not necessarily visit every vertex or face that is a member of a particular graph.
MeshGraph
s also directly expose entities by type via iterators, but without a deterministic ordering. These categorical iterators are always exhaustive, and visit all topological structures in a graph regardless of their connectivity.
Mutable iterators (including circulators) emit orphan views, because mutable views require exclusive access. To mutate topology using multiple mutable views, use an immutable circulator to collect the keys of the target topology and then lookup each mutable view using those keys.
Note
Mutations may be dependent and invalidate keys. Some mutations may not be able to operate on the given set of keys as trivially as seen in the example above. Poking a face is an independent mutation and does not affect other faces in a graph.
Topological Mutations¶
Mutable views expose topological mutations that alter the structure of a graph. These operations are always consuming, because they often invalidate the view that initiates them.
Most mutations return a view over a modified or newly inserted topological structure that can be used to further traverse the graph. For example, splitting an arc \overrightarrow{AB} returns a vertex M that subdivides the composite edge. The leading arc of M is \overrightarrow{MB} and is a part of the same ring as the initiating arc.
It is possible to downgrade mutable views into immutable views using into_ref
. This can be useful when performing topological mutations, as it allows for any number of traversals immediately after the mutation is performed.
Graphs also provide topological mutations that may operate over an entire graph.
Graphs may contain disjoint sub-graphs, which are topological groups that cannot reach each other. It is possible to split a graph into sub-graphs.
Topological mutations expose spatial functions for types that implement geometric traits, such as split_at_midpoint
. Views also expose purely topological functions, which can always be used (if, for example, the data in a graph is non-spatial).
In the above example, split_with
accepts a function that returns data for the subdividing vertex of the split. Similar functions exist for other topological mutations as well, such as poke_with
.
Computation vs. Payload¶
When graph data implements geometric traits, views expose methods to compute related attributes like normals and centroids.
These computations are based on the positional data in vertices. However, it is also possible to include these attributes in the data of a graph and assign arbitrary values as needed. For example, it is sometimes desirable to establish vertex normals independently of surrounding face or edge geometry.
The above example uses the Vertex
type to store a position and normal in each vertex. This is distinct from computed attributes, as this normal is arbitrary and is not recomputed when it is accessed. Applications may choose either approach, though computation is typically preferable.
Generic Programming¶
The graph
module provides traits that express the geometric capabilities of a MeshGraph
. These can be used to write generic code that requires particular geometric operations, such as computing edge midpoints. This example subdivides a face in a mesh by splitting arcs at their midpoints:
These traits avoid the need to specify very complex type bounds, but it is also possible to express type bounds directly using traits from the [decorum
][] and [theon
][] crates.
The following example expresses type bounds for a function that computes the area of faces in two-dimensional graphs:
Some entities and meta-entities are strongly related and have common semantics. These entities and their views can be abstracted via traits. The ToArc
trait is implemented by ArcView
and EdgeView
and allows either type to be converted into an arc that participates in its composite edge. Similarly, the ToRing
trait is implemented by FaceView
and Ring
and allows either type to be converted into its associated ring. Geometric operation traits like EdgeMidpoint
use these traits to accept related entities, for example.