Advanced Input & Output

Batch unions/differences across many placements; scale, position, iterate, and export indexed topology.

This example demonstrates advanced file I/O combined with batched Booleans:

  • Read two meshes (icosahedron.obj, sphere.obj)
  • Generate many contact placements (sphere at each triangle centroid)
  • Try multiple sphere scales and both union/difference
  • Iterate: import translated sphere → Boolean with current result → materialize
  • Export indexed triangles (with shared vertices) per run

It showcases:

Note:
Uses ExampleFramework.hh for compact POD types and utilities (readFromOBJ, writeToOBJ, small math helpers).

Result

A sphere subtracted from each face of an icosahedron

CMake setup

To use the C++ 17 API from CMake, add the Solidean C++ 17 language target and link it to your project:

add_subdirectory(path/to/solidean/lang/cpp17)              # Add the Solidean C++ 17 API to your project
target_link_libraries(YourProject PRIVATE Solidean::Cpp17) # Link against the Solidean C++ 17 API

Code

#include <chrono>
#include <cmath>
#include <cstring>
#include <filesystem>
#include <iostream>
#include <vector>

#include <solidean.hh>

#include "ExampleFramework.hh"


int main()
{
    auto const basePath = std::filesystem::path(__FILE__).parent_path();

    // Read two input meshes
    auto const icosahedron = example::readFromOBJ(basePath / "icosahedron.obj");
    auto const sphere = example::readFromOBJ(basePath / "sphere.obj");

    // Print some information on the inputs
    std::cout << "Number of triangles in icosahedron: " << icosahedron.size() << std::endl;
    std::cout << "Number of triangles in sphere: " << sphere.size() << std::endl;

    // Define some positions where we want the sphere mesh to interact with the icosahedron mesh
    std::vector<example::pos3> sphereContactPositions;
    for (auto const& tri : icosahedron)
    {
        auto const triCenter = (tri.p0 + tri.p1 + tri.p2) / 3.0;
        sphereContactPositions.push_back(triCenter);
    }

    // compute the maximum absolute coordinates to define the exact arithmetic
    auto const getMaxCoord = [](std::vector<example::triangle> const& triangles)
    {
        float maxCoord = std::abs(triangles.front().p0.x);
        for (auto const& t : triangles)
        {
            auto const max0 = std::max({std::abs(t.p0.x), std::abs(t.p0.y), std::abs(t.p0.z)});
            auto const max1 = std::max({std::abs(t.p1.x), std::abs(t.p1.y), std::abs(t.p1.z)});
            auto const max2 = std::max({std::abs(t.p2.x), std::abs(t.p2.y), std::abs(t.p2.z)});
            maxCoord = std::max({maxCoord, max0, max1, max2});
        }
        return maxCoord;
    };
    auto const maxCoordIcosahedron = getMaxCoord(icosahedron);
    auto const maxCoordSphere = getMaxCoord(sphere);


    auto ctx = solidean::Context::create();

    for (auto sphereScaling : {0.25f, 0.4f, 0.5f})
    {
        auto scaledSphere = sphere;
        for (auto& tri : scaledSphere)
        {
            tri.p0 *= sphereScaling;
            tri.p1 *= sphereScaling;
            tri.p2 *= sphereScaling;
        }

        // As long as the two meshes (after translating, see below) at least touch each other, this coord will not be exceeded
        auto const maxCoord = maxCoordIcosahedron + getMaxCoord(scaledSphere);
        auto arithmetic = ctx->createExactArithmetic(maxCoord);

        for (auto isUnion : {true, false})
        {
            std::cout << std::endl;
            std::cout << "Computing " << (isUnion ? "union" : "difference") << " with spheres with radius " << sphereScaling << std::flush;

            // Create the actual solidean mesh from the icosahedron triangles,
            // the triangle data is reinterpreted as a solidean triangle type
            auto icomesh = ctx->createMeshFromTrianglesF32(solidean::as_triangle3_span(icosahedron), *arithmetic);

            // Measure time of actual solidean task
            auto const start = std::chrono::high_resolution_clock::now();

            // Iterate over the contact positions
            for (auto const& p : sphereContactPositions)
            {
                // Create "tool" triangles at current contact position
                auto toolTriangles = scaledSphere;
                for (auto& t : toolTriangles)
                {
                    t.p0 += p;
                    t.p1 += p;
                    t.p2 += p;
                }

                // Perform one union or subtraction iteration (icosahedron +/- sphere)
                icomesh = ctx->execute( //
                    *arithmetic,
                    [&](solidean::Operation& op)
                    {
                        auto meshA = op.input(*icomesh);
                        auto meshB = op.importFromTrianglesF32(solidean::as_triangle3_span(toolTriangles));

                        if (isUnion)
                            return op.output(op.union_(meshA, meshB));

                        return op.output(op.difference(meshA, meshB));
                    });

                std::cout << "." << std::flush;
            }
            std::cout << std::endl;

            auto const end = std::chrono::high_resolution_clock::now();
            auto const duration = std::chrono::duration<double>(end - start).count();

            std::cout << "Processed " << sphereContactPositions.size() << " iterations in " << duration * 1000.0 << "ms (~"
                      << duration * 1000.0 / sphereContactPositions.size() << "ms per iteration)" << std::endl;

            // Export the final solidean data to floating point triangles for potential further evaluation or display
            auto blob = ctx->execute(*arithmetic,
                                     [&](solidean::Operation& op)
                                     {
                                         auto m = op.input(*icomesh);
                                         return op.exportToIndexedTrianglesF32(m);
                                     });

            // The data blob contains (immutable) indexed triangle data (i.e. topology exists)
            auto const triangleIndexSpan = blob->getTrianglesIndexed<example::tri_idx>();
            auto const vertexPositionSpan = blob->getPositionsF32<example::pos3>();

            // Copy the triangle data to a vector, e.g. for further processing
            auto const indices = std::vector<example::tri_idx>(triangleIndexSpan.begin(), triangleIndexSpan.end());
            auto const positions = std::vector<example::pos3>(vertexPositionSpan.begin(), vertexPositionSpan.end());

            std::cout << "Total number of triangles in result mesh: " << indices.size() << std::endl;

            auto const getScalingString = [](float scaling)
            {
                std::ostringstream out;
                out << std::fixed << std::setprecision(2) << scaling;
                return out.str();
            };

            auto const fileName = (isUnion ? "result_union" : "result_difference") + getScalingString(sphereScaling) + ".obj";

            // Write the result to an obj file
            example::writeToOBJ(positions, indices, basePath / fileName);
        }
    }

    return EXIT_SUCCESS;
}

Notes

  • Exact arithmetic sizing:
    For each sphere scale, the example computes a safe bound as
    maxCoord = maxCoordIcosahedron + getMaxCoord(scaledSphere) and calls
    Context::createExactArithmetic.
    This keeps the bound tight for precision while accommodating translations.

  • Placement generation:
    Sphere placements come from triangle centroids of the icosahedron, producing a dense set of contact points.

  • Iterative command buffer:
    Each iteration records and executes:
    Operation::input → import translated sphere → Operation::union or Operation::differenceOperation::output.
    The resulting persistent Mesh becomes the input to the next iteration.

  • Indexed export for topology:
    Using exportToIndexedTrianglesF32 deduplicates vertices and resolves T-junctions.
    Better for storage, connectivity, and downstream processing.

  • Geometry guarantees:
    If source meshes might self-intersect or overlap, import with MeshType::Supersolid, or run a preprocessing
    Operation::selfUnion / Operation::heal pass first.

  • Output management:
    The example writes one OBJ per (scale, op) pair (e.g. result_union0.25.obj, result_difference0.40.obj).