rf-geom / ridgeforge Public

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.

ValueRaised when
Okthe call did what it said
FileNotFoundthe path could not be opened for reading
ParseErrorthe file opened but its contents are not valid for the format
UnsupportedFormatextension recognised, variant not handled, for example PLY with double coordinates
EmptyMeshzero triangles where at least one is required
NonManifoldthe operation needs manifold input and the mesh is not
InvalidOptionan option struct contradicts itself, for example both targets set to zero
OutOfRangea triangle references a vertex index past the end of positions
Cancelleda Progress callback returned false
WriteFailedthe stream reported an error while writing

template <class T> class Result

explicit operator bool() const noexcept; // true when status() == Status::Ok Status status() const noexcept; const char* message() const noexcept; // static string, never null T& value(); // undefined unless bool(*this) T& operator*(); // same, without the name

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.

MeshView(const Vec3* pos, std::size_t nverts, const Tri* tris, std::size_t ntris) noexcept; MeshView(const Mesh&) noexcept;

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);
};
Read the guards

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.

std::size_t weld_vertices(Mesh&, Scalar tolerance); std::size_t remove_degenerate(Mesh&, Scalar min_area); std::size_t remove_duplicate_triangles(Mesh&); std::size_t remove_small_components(Mesh&, std::size_t min_triangles); Result<std::size_t> orient_consistently(Mesh&); Result<std::size_t> fill_holes(Mesh&, std::size_t max_loop_edges);
weld_vertices(mesh, tolerance)
Merges vertices closer than tolerance in 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 by remove_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 absolute 1e-12 on 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::NonManifold if 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_edges edges 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

FieldTypeDefaultMeaning
target_trianglesstd::size_t0stop when the count reaches this; 0 means no count target
target_ratiofloat0.0ffraction of the input count, 0 to 1; ignored when target_triangles is set
max_errorScalar0refuse collapses whose quadric error exceeds this; 0 means unbounded
preserve_boundarybooltruenever move or remove a vertex on a boundary loop
preserve_ridgesboolfalselock vertices on edges sharper than ridge_angle_deg
ridge_angle_degfloat40.0fdihedral threshold for the above, in degrees
max_aspect_ratiofloat20.0freject a collapse that would create a sliver worse than this
allow_topology_changeboolfalsepermit collapses that merge separate boundary loops
locked_verticesconst std::vector<Index>*nullptrextra vertices to pin; not copied, must outlive the run
progressProgressnullptrcalled about every 2 % of the way
threadsunsigned00 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

explicit Decimator(DecimateOptions opt); Result<DecimateReport> run(Mesh& mesh); struct DecimateReport { std::size_t collapses; // edges actually collapsed std::size_t triangles_before; std::size_t triangles_after; double max_applied_error; // largest quadric error accepted bool target_reached; // false when locks blocked the target };

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.

struct RidgeOptions { float min_angle_deg = 35.0f; // edges sharper than this qualify float max_angle_deg = 175.0f; // above this, treat as a crease artefact std::size_t min_edges = 4; // discard shorter chains bool include_boundary = true; // emit boundary loops as RidgeKind::Boundary unsigned smooth_passes = 0; // Laplacian passes along each line bool split_at_junctions = true; // cut where three or more lines meet }; enum class RidgeKind { Ridge, Valley, Boundary }; struct Polyline { std::vector<Index> vertices; RidgeKind kind; float mean_angle_deg; bool closed; }; struct RidgeSet { std::vector<Polyline> lines; }; explicit RidgeExtractor(RidgeOptions opt); Result<RidgeSet> run(const Mesh& mesh) const;
kind
Ridge for a convex edge, Valley for 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.

struct TriPair { Index a, b; }; struct FindOptions { bool ignore_adjacent = true; // skip pairs sharing a vertex or edge std::size_t max_pairs = 0; // stop early; 0 means find all unsigned threads = 0; }; struct ResolveOptions { unsigned max_passes = 3; bool allow_vertex_move = true; Scalar move_limit = 0; // 0 means one average edge length }; struct ResolveReport { std::size_t pairs_before, pairs_after, vertices_moved, triangles_removed; }; Result<std::vector<TriPair>> find(MeshView, const FindOptions& = {}); Result<ResolveReport> resolve(Mesh&, const ResolveOptions& = {});
resolve is experimental

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.

struct RemeshOptions { Scalar target_edge_length = 0; // 0 means the input mean edge length unsigned iterations = 5; float feature_angle_deg = 40.0f; // edges above this stay put bool project_to_input = true; // snap back onto a copy of the input bool preserve_boundary = true; Progress progress = nullptr; }; explicit Remesher(RemeshOptions opt); Result<void> run(Mesh& mesh);

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.

enum class Format { Auto, Obj, Ply, Stl }; struct WriteOptions { Format format = Format::Auto; bool binary = true; // ignored for OBJ int float_digits = 7; // ascii output only bool write_normals = false; }; Result<Mesh> read(const std::string& path); Result<Mesh> read(const std::string& path, Format); Result<void> write(const Mesh&, const std::string& path); Result<void> write(const Mesh&, const std::string& path, const WriteOptions&);
FormatReadWriteCarried through
OBJasciiasciipositions, normals, one UV set; groups and materials are dropped
PLYascii, binary little and big endianbinary or asciipositions, normals, UV under the usual property names
STLbinary, asciibinarypositions only; the file has no vertex sharing, so reading one implies welding afterwards

Progress and cancellation

using Progress = std::function<bool(float fraction, const char* stage)>;

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.