reference
Namespace rf
Everything public lives in rf and is reachable through one header per area. There is no master include, because pulling in the intersection hierarchy to load an OBJ file would be rude.
Basic types
Declared in <ridgeforge/types.hpp>, included by every other header.
namespace rf {
using Index = std::uint32_t; // 2^31 - 1 vertices maximum
using Scalar = float; // double with RF_DOUBLE_PRECISION=ON
struct Vec3 { Scalar x, y, z; };
struct Vec2 { Scalar u, v; };
struct Tri { Index v[3]; };
struct AABB { Vec3 lo, hi; bool empty() const; Vec3 extent() const; };
}
enum class Status
Returned inside every Result. Ten values, and the only one that means success is the first.
| Value | Raised when |
|---|---|
Ok | the call did what it said |
FileNotFound | the path could not be opened for reading |
ParseError | the file opened but its contents are not valid for the format |
UnsupportedFormat | extension recognised, variant not handled, for example PLY with double coordinates |
EmptyMesh | zero triangles where at least one is required |
NonManifold | the operation needs manifold input and the mesh is not |
InvalidOption | an option struct contradicts itself, for example both targets set to zero |
OutOfRange | a triangle references a vertex index past the end of positions |
Cancelled | a Progress callback returned false |
WriteFailed | the stream reported an error while writing |
template <class T> class Result
There is no Result<void> specialisation trick: it exists and carries only the status. The message() text is meant for a log line, not for parsing, and it is the only part of the API that may change wording in a patch release.
class Mesh
<ridgeforge/mesh.hpp>. A plain struct with methods, not an opaque handle. Fill the vectors yourself if your data comes from somewhere the I/O layer does not know about.
class Mesh {
public:
std::vector<Vec3> positions;
std::vector<Tri> triangles;
std::vector<Vec3> normals; // empty, or exactly positions.size()
std::vector<Vec2> uv; // empty, or exactly positions.size()
std::size_t vertex_count() const noexcept;
std::size_t triangle_count() const noexcept;
bool has_normals() const noexcept;
bool has_uv() const noexcept;
AABB bounds() const noexcept;
void clear() noexcept;
void reserve(std::size_t verts, std::size_t tris);
Result<void> validate() const;
};
- bounds()
- Recomputed on every call in linear time; nothing is cached, because a mutable public vector cannot be watched. Returns an empty box for an empty mesh rather than an inverted one.
- validate()
- Checks that every index is in range, that no coordinate is NaN or infinite and that the optional attribute vectors have a legal size. Linear, allocation-free, and worth calling once after you build a mesh by hand.
class MeshView
A non-owning pair of spans for algorithms that only read. Constructible from a Mesh implicitly, or from raw pointers when your geometry lives in someone else's buffer.
Stats::of and SelfIntersection::find accept a view. The mutating operations do not, for the obvious reason.
struct Stats
<ridgeforge/stats.hpp>. One pass over the mesh, one answer to most of the questions people ask before deciding what to run.
struct Stats {
std::size_t vertices, triangles, edges;
std::size_t boundary_loops, components;
std::size_t non_manifold_edges, non_manifold_vertices;
int euler_characteristic;
int genus; // meaningful only if closed && manifold
bool closed, manifold, oriented;
double area;
double volume; // meaningful only if closed
AABB bounds;
static Stats of(MeshView);
};
genus is -1 and volume is 0.0 when their precondition does not hold. They are not errors and they do not set a status, so a program that prints the genus of an open scan will print nonsense unless it checks closed first.
namespace Repair
<ridgeforge/repair.hpp>. Free functions, each one small enough to reason about, each returning how much it changed.
- weld_vertices(mesh, tolerance)
- Merges vertices closer than
tolerancein every axis, using a uniform grid of that cell size. Returns the number of vertices removed. A tolerance of zero merges exact duplicates only, which is the right choice for STL input. Welding is the one operation that can create non-manifold edges out of clean input, so it is usually followed byremove_degenerate. - remove_degenerate(mesh, min_area)
- Drops triangles whose area is at or below
min_area, including the ones with two equal indices. Returns the count removed. Pass a value relative to your model scale; an absolute1e-12on a mesh measured in kilometres removes nothing. - orient_consistently(mesh)
- Flood-fills each component and flips triangles to agree with the first one seen. Returns the number flipped. Fails with
Status::NonManifoldif an edge has more than two incident faces, because then there is no consistent answer. Does not guarantee outward orientation; for a closed component it then checks the signed volume and flips the whole component if it came out negative. - fill_holes(mesh, max_loop_edges)
- Triangulates every boundary loop with at most
max_loop_edgesedges by ear clipping in the loop's best-fit plane. Returns the number of loops closed. Loops that are not nearly planar get a fan to their centroid instead, which is ugly but watertight. Passing zero closes nothing and is not an error. - remove_small_components(mesh, min_triangles)
- Deletes connected components below the threshold and returns the number of components deleted. The usual way to get rid of scanner speckle before decimating.
class Decimator
<ridgeforge/decimate.hpp>. Quadric error edge collapse. The algorithm and the exact error measure are described on the algorithms page.
struct DecimateOptions
| Field | Type | Default | Meaning |
|---|---|---|---|
target_triangles | std::size_t | 0 | stop when the count reaches this; 0 means no count target |
target_ratio | float | 0.0f | fraction of the input count, 0 to 1; ignored when target_triangles is set |
max_error | Scalar | 0 | refuse collapses whose quadric error exceeds this; 0 means unbounded |
preserve_boundary | bool | true | never move or remove a vertex on a boundary loop |
preserve_ridges | bool | false | lock vertices on edges sharper than ridge_angle_deg |
ridge_angle_deg | float | 40.0f | dihedral threshold for the above, in degrees |
max_aspect_ratio | float | 20.0f | reject a collapse that would create a sliver worse than this |
allow_topology_change | bool | false | permit collapses that merge separate boundary loops |
locked_vertices | const std::vector<Index>* | nullptr | extra vertices to pin; not copied, must outlive the run |
progress | Progress | nullptr | called about every 2 % of the way |
threads | unsigned | 0 | 0 means hardware concurrency; only the quadric pass is parallel |
At least one of target_triangles, target_ratio and max_error must be non-zero, otherwise run returns Status::InvalidOption without touching the mesh.
Methods
A Decimator is reusable: constructing it once and calling run on several meshes is cheap, since no state survives a run. target_reached == false with a status of Ok is the normal outcome for a mesh whose features are all locked - the mesh is valid, it is just bigger than you asked for.
class RidgeExtractor
<ridgeforge/ridge.hpp>. Finds sharp edges and returns them as ordered polylines rather than as an unsorted edge set, which is the part that takes the work.
- kind
Ridgefor a convex edge,Valleyfor a concave one, decided by the sign of the dihedral angle against the face normals. A chain that changes sign is split at the sign change, so no polyline is half ridge and half valley.- smooth_passes
- Smooths the polyline positions only, never the mesh. Useful for drawing; useless as input to anything that must stay on the surface, because after one pass the line no longer lies on it.
- closed
- True when the first and last vertex coincide, as happens on the rim of a cylinder. The repeated vertex is not stored twice.
namespace SelfIntersection
<ridgeforge/selfint.hpp>. Detection is exact and has no tolerance parameter; resolution is best effort and says so in its name.
It moves vertices out of contact and, failing that, deletes the smaller of the two offending triangles and re-closes the hole. On two shells nested inside one another it will churn through its passes and report pairs_after unchanged. Detection, by contrast, is exact: it reports a pair if and only if the two closed triangles share a point.
class Remesher
<ridgeforge/remesh.hpp>. Isotropic remeshing by split, collapse, flip and tangential relaxation, repeated for a fixed number of iterations.
With project_to_input on, the remesher keeps a bounding-volume hierarchy over the original triangles and projects every moved vertex back onto it, which costs about 40 % of the run time and is the difference between a remesh and a slow smoothing.
namespace io
<ridgeforge/io.hpp>. Three formats, chosen by extension unless you say otherwise.
| Format | Read | Write | Carried through |
|---|---|---|---|
| OBJ | ascii | ascii | positions, normals, one UV set; groups and materials are dropped |
| PLY | ascii, binary little and big endian | binary or ascii | positions, normals, UV under the usual property names |
| STL | binary, ascii | binary | positions only; the file has no vertex sharing, so reading one implies welding afterwards |
Progress and cancellation
Return true to continue, false to stop. A cancelled operation leaves the mesh in a valid but intermediate state - decimation that was asked to stop has done fewer collapses, not an invalid half-collapse - and returns Status::Cancelled. The stage string is one of "quadrics", "collapse", "split", "relax", "project" or "search"; it is a literal and safe to compare by pointer within one library build, though comparing the text is wiser.
The callback is invoked from the calling thread only, even when threads is greater than one, so it does not need to be thread safe.