Example Framework Header

A tiny helper header for demos and quick tests, showcasing custom types and mesh utilities.

The SDK ships with a small single-header utility library called ExampleFramework.hh.
It is not part of the Solidean API itself, but provides convenience types and helpers for examples, demos, and quick experiments.
It also demonstrates how Solidean can be used with pre-existing types.

Features

  • Minimal custom data types:
    • pos3: a simple float3 (x, y, z)
    • tri_idx: three integers forming a triangle index
    • triangle: three pos3 vertices
  • Basic vector helper (length, dot, cross, normalize, …)
  • Helpers to work with triangle meshes:
    • Unroll indexed triangles (positions + indices → vector<triangle>)
    • Compute surface area and volume
      (Solidean has these as queries but the purpose is to show "external verification")
  • Simple geometry constructors:
    • createCube
    • createIcoSphere
  • Simple OBJ input/output:
    • readFromOBJ
    • writeToOBJ

Purpose

This header is meant as a lightweight testing scaffold:

  • Lets you drop in trivial custom types without pulling external math libraries.
  • Ensures all code examples are self-contained and compile out of the box.
  • Shows how to integrate Solidean with your own data structures.

For production code, you should use your project’s preferred math and geometry libraries.
ExampleFramework.hh is for illustration and quick prototyping only.

Full Code

#pragma once

#include <cmath>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>

namespace example
{
// Simple data types for a single precision 3D position, triangle indices, and for an unrolled triangle
struct pos3
{
    float x;
    float y;
    float z;
    pos3 operator*(float factor) const { return {factor * x, factor * y, factor * z}; }
    pos3& operator*=(float factor)
    {
        x *= factor;
        y *= factor;
        z *= factor;
        return *this;
    }
    pos3 operator/(float factor) const { return {x / factor, y / factor, z / factor}; }
    pos3 operator+(pos3 const& other) const { return {x + other.x, y + other.y, z + other.z}; }
    pos3 operator+=(pos3 const& other)
    {
        x += other.x;
        y += other.y;
        z += other.z;
        return *this;
    }
    pos3 operator-(pos3 const& other) const { return {x - other.x, y - other.y, z - other.z}; }
    friend pos3 operator*(float factor, pos3 const& v) { return {factor * v.x, factor * v.y, factor * v.z}; }
};

struct tri_idx
{
    int i0;
    int i1;
    int i2;
};
struct triangle
{
    pos3 p0;
    pos3 p1;
    pos3 p2;
};

// Functions for 3D positions/vectors
[[nodiscard]] inline float length(pos3 const& pos) { return std::sqrt(pos.x * pos.x + pos.y * pos.y + pos.z * pos.z); }
[[nodiscard]] inline float dot(pos3 const& a, pos3 const& b) { return a.x * b.x + a.y * b.y + a.z * b.z; }
[[nodiscard]] inline pos3 cross(pos3 const& a, pos3 const& b) { return {a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x}; }
[[nodiscard]] inline pos3 normalize(pos3 const& pos)
{
    auto const len = length(pos);
    if (len == 0.0)
        return pos;

    return {pos.x / len, pos.y / len, pos.z / len};
}

// Creates unrolled triangles of an indexed mesh
[[nodiscard]] inline std::vector<triangle> unrollTriangles(std::vector<pos3> const& vertices, std::vector<tri_idx> const& indices)
{
    std::vector<triangle> result;
    result.reserve(indices.size());
    for (auto [i0, i1, i2] : indices)
    {
        auto const& v0 = vertices[i0];
        auto const& v1 = vertices[i1];
        auto const& v2 = vertices[i2];
        result.push_back({v0, v1, v2});
    }
    return result;
}

// Computes the surface area and the enclosed volume of the provided unrolled triangle mesh
[[nodiscard]] inline std::pair<double, double> computeAreaAndVolume(std::vector<triangle> const& triangles)
{
    double area = 0.0;
    double volume = 0.0;
    for (auto const& [v0, v1, v2] : triangles)
    {
        area += length(cross(v1 - v0, v2 - v0)) / 2;
        volume += dot(cross(v1, v2), v0) / 6;
    }
    return {area, volume};
}

// Computes the surface area and the enclosed volume of the provided indexed triangle mesh
[[nodiscard]] inline std::pair<double, double> computeAreaAndVolume(std::vector<pos3> const& vertices, std::vector<tri_idx> const& indices)
{
    return computeAreaAndVolume(unrollTriangles(vertices, indices));
}

// Creates triangles of a cube with side length <sideLength> and centered at <center>
[[nodiscard]] inline std::vector<triangle> createCube(pos3 center = {0.0f, 0.0f, 0.0f}, float sideLength = 1.0)
{
    // Define the indices for the cube
    std::vector<tri_idx> indices = {
        {0, 1, 2}, // front face
        {0, 2, 3}, //
        {4, 6, 5}, // back face
        {4, 7, 6}, //
        {1, 5, 6}, // right face
        {1, 6, 2}, //
        {0, 7, 4}, // left face
        {0, 3, 7}, //
        {3, 2, 6}, // top face
        {3, 6, 7}, //
        {0, 5, 1}, // bottom face
        {0, 4, 5},
    };

    // Define the vertices for the cube
    std::vector<pos3> vertices = {
        {-0.5f, 0.5f, -0.5f},  // vertex 0
        {0.5f, 0.5f, -0.5f},   // vertex 1
        {0.5f, -0.5f, -0.5f},  // vertex 2
        {-0.5f, -0.5f, -0.5f}, // vertex 3
        {-0.5f, 0.5f, 0.5f},   // vertex 4
        {0.5f, 0.5f, 0.5f},    // vertex 5
        {0.5f, -0.5f, 0.5f},   // vertex 6
        {-0.5f, -0.5f, 0.5f}   // vertex 7
    };

    // scale and move
    for (auto& v : vertices)
        v = center + v * sideLength;

    return unrollTriangles(vertices, indices);
}

// Creates triangles of a sphere mesh from a subdivided icosahedron
[[nodiscard]] inline std::vector<triangle> createIcoSphere(int subdivs, pos3 center = {0.0f, 0.0f, 0.0f}, float radius = 1.0)
{
    if (subdivs < 0)
    {
        // empty vector
        return {};
    }
    constexpr auto X = 0.525731112119133606f;
    constexpr auto Z = 0.850650808352039932f;
    std::vector<pos3> vertices = {{-X, 0.0f, Z}, {X, 0.0f, Z},   {-X, 0.0f, -Z}, {X, 0.0f, -Z}, {0.0f, Z, X},  {0.0f, Z, -X},
                                  {0.0f, -Z, X}, {0.0f, -Z, -X}, {Z, X, 0.0f},   {-Z, X, 0.0f}, {Z, -X, 0.0f}, {-Z, -X, 0.0f}};
    std::vector<tri_idx> indices
        = {{1, 4, 0},  {4, 9, 0},  {4, 5, 9},  {8, 5, 4},  {1, 8, 4}, {1, 10, 8}, {10, 3, 8}, {8, 3, 5},  {3, 2, 5}, {3, 7, 2},
           {3, 10, 7}, {10, 6, 7}, {6, 11, 7}, {6, 0, 11}, {6, 1, 0}, {10, 1, 6}, {11, 0, 9}, {2, 11, 9}, {5, 2, 9}, {11, 2, 7}};

    auto const normalizedAverage = [](example::pos3 const& v0, example::pos3 const& v1) -> example::pos3
    {
        example::pos3 vecSum = {v0.x + v1.x, v0.y + v1.y, v0.z + v1.z};
        auto const len = std::sqrt(vecSum.x * vecSum.x + vecSum.y * vecSum.y + vecSum.z * vecSum.z);
        if (len < 1e-6)
            return vecSum;

        return {float(vecSum.x / len), float(vecSum.y / len), float(vecSum.z / len)};
    };

    for (auto d = subdivs; d > 0; --d)
    {
        std::vector<example::tri_idx> newIndices;
        newIndices.reserve(indices.size() * 4);
        for (auto const& t : indices)
        {
            // edge midpoints normalized to length one (unit icosphere)
            auto const mid1 = normalizedAverage(vertices[t.i0], vertices[t.i1]);
            auto const mid2 = normalizedAverage(vertices[t.i1], vertices[t.i2]);
            auto const mid3 = normalizedAverage(vertices[t.i2], vertices[t.i0]);

            // create three new vertex indices
            int mid1Idx = int(vertices.size());
            int mid2Idx = mid1Idx + 1;
            int mid3Idx = mid1Idx + 2;

            // add three new vertices
            vertices.push_back(mid1);
            vertices.push_back(mid2);
            vertices.push_back(mid3);

            // every original triangle is replaced by four new ones
            newIndices.push_back({t.i0, mid1Idx, mid3Idx});
            newIndices.push_back({mid1Idx, t.i1, mid2Idx});
            newIndices.push_back({mid3Idx, mid2Idx, t.i2});
            newIndices.push_back({mid1Idx, mid2Idx, mid3Idx});
        }

        indices = newIndices;
    }

    // scale and move
    for (auto& v : vertices)
        v = center + v * radius;

    return unrollTriangles(vertices, indices);
}

inline void writeToOBJ(std::vector<pos3> const& vertices, std::vector<tri_idx> const& indices, std::filesystem::path const& filename)
{
    std::ofstream ofs(filename);
    if (!ofs.is_open())
    {
        std::cerr << "Could not open " << filename << " for writing." << std::endl;
        return;
    }

    std::string content;

    for (auto const& v : vertices)
        content += "v " + std::to_string(v.x) + " " + std::to_string(v.y) + " " + std::to_string(v.z) + "\n";

    for (auto const& f : indices)
        content += "f " + std::to_string(f.i0 + 1) + " " + std::to_string(f.i1 + 1) + " " + std::to_string(f.i2 + 1) + "\n";

    ofs << content;
}

inline void writeToOBJ(std::vector<triangle> const& triangles, std::filesystem::path const& filename)
{
    // create vertices and indices without merging any vertices
    std::vector<tri_idx> indices(triangles.size());
    std::vector<pos3> vertices(3 * triangles.size());
    for (auto i = 0; i < indices.size(); ++i)
    {
        indices[i] = {3 * i + 0, 3 * i + 1, 3 * i + 2};
        vertices[3 * i + 0] = triangles[i].p0;
        vertices[3 * i + 1] = triangles[i].p1;
        vertices[3 * i + 2] = triangles[i].p2;
    }

    writeToOBJ(vertices, indices, filename);
}

inline std::vector<triangle> readFromOBJ(std::filesystem::path const& filename)
{
    std::ifstream ifs(filename, std::ios::binary | std::ios::ate);
    if (!ifs.is_open())
    {
        std::cerr << "Could not open " << filename << " for reading." << std::endl;
        return {};
    }

    std::streamsize size = ifs.tellg();
    ifs.seekg(0, std::ios::beg);
    std::string buffer(size, '\0');
    if (!ifs.read(&buffer[0], size))
    {
        std::cerr << "Could not read file " << filename << "." << std::endl;
        return {};
    }

    std::istringstream input(buffer);
    std::vector<pos3> vertices;
    std::vector<triangle> result;

    std::string line;
    while (std::getline(input, line))
    {
        // empty line or comment
        if (line.empty() || line[0] == '#')
            continue;

        std::istringstream linestream(line);
        std::string prefix;
        linestream >> prefix;

        if (prefix == "v")
        {
            pos3 v{};
            linestream >> v.x >> v.y >> v.z;
            vertices.push_back(v);
        }
        else if (prefix == "f")
        {
            tri_idx idx{};
            for (auto c : {&idx.i0, &idx.i1, &idx.i2})
            {
                std::string token;
                linestream >> token;
                std::istringstream tokenStream(token);
                std::string indexStr;
                // only the first value (vertex index) is considered, normals/texcoords are ignored
                std::getline(tokenStream, indexStr, '/');
                *c = std::stoi(indexStr) - 1; // 0-based
            }

            triangle tri{vertices[idx.i0], vertices[idx.i1], vertices[idx.i2]};
            result.push_back(tri);
        }
    }

    return result;
}

} // namespace example