Evaluating CSG Trees

Two patterns: single-shot evaluation with one Operation, and an iterated/persistent version that materializes subresults.

This guide shows two pragmatic ways to evaluate a CSG tree into a mesh:

  • A) Single-shot: build one Operation that imports all leaves and records the whole Boolean expression, then export once.
  • B) Iterated/persistent: recursively materialize subtrees as persistent Mesh objects and combine them node-by-node.

Minimal CSG node type (for both versions)

#include <memory>
#include <vector>
#include <solidean.hh>
#include "ExampleFramework.hh" // example::triangle

enum class CsgKind 
{
    LeafTriangles,
    Union,
    Difference,
    Intersection
};

struct CsgNode 
{
    CsgKind kind = CsgKind::LeafTriangles;
    std::vector<example::triangle> triangles; // non-empty for leaves
    std::shared_ptr<CsgNode>       childA;    // non-null iff !leaf
    std::shared_ptr<CsgNode>       childB;    // non-null iff !leaf
};

A) Single-shot (one Operation for the entire tree)

Idea: build a single command buffer. Each leaf is imported as a MeshOperand; internal nodes combine child operands using the Boolean of the node. Finally, export once.

solidean::MeshOperand eval_to_operand(solidean::Operation& op,
                                      CsgNode const& node,
                                      solidean::MeshType leafType)
{
    // Import leaf triangles into the operation
    if (node.kind == CsgKind::LeafTriangles)
        return op.importFromTrianglesF32(solidean::as_triangle3_span(node.triangles), leafType);

    // Recurse
    auto a = eval_to_operand(op, *node.childA, leafType);
    auto b = eval_to_operand(op, *node.childB, leafType);

    switch (node.kind) 
    {
        case CsgKind::Union:        return op.union_(a, b);        // Boolean union
        case CsgKind::Difference:   return op.difference(a, b);    // A - B
        case CsgKind::Intersection: return op.intersection(a, b);  // A ∩ B
        default:                    return a; // unreachable for our enum
    }
}

// Evaluate tree in one shot and export unrolled triangles
std::vector<example::triangle> evaluate_csg_single_shot(solidean::Context& ctx,
                                                        solidean::ExactArithmetic& arithmetic,
                                                        CsgNode const& root,
                                                        solidean::MeshType leafType = solidean::MeshType::Solid)
{
    auto blob = ctx.execute(arithmetic, [&](solidean::Operation& op)
    {
        auto r = eval_to_operand(op, root, leafType);
        // Export once at the root
        return op.exportToTrianglesF32(r);
    });

    auto span = blob->getTrianglesF32<example::triangle>();
    return std::vector<example::triangle>(span.begin(), span.end());
}

Notes

B) Iterated / persistent (materialize subresults)

Idea: recursively evaluate each subtree into a persistent Mesh. Non-leaf nodes run a tiny one-off operation that takes two inputs, applies the Boolean, and outputs a persistent mesh. This is a good fit if you want to cache subtrees or inspect intermediate results.

std::shared_ptr<solidean::Mesh> eval_to_mesh(solidean::Context& ctx,
                                             solidean::ExactArithmetic& arithmetic,
                                             CsgNode const& node,
                                             solidean::MeshType leafType)
{
    using K = CsgKind;

    if (node.kind == CsgKind::LeafTriangles) 
    {
        // Materialize a persistent mesh for a leaf
        auto m = ctx.createMeshFromTrianglesF32(solidean::as_triangle3_span(node.triangles), arithmetic, leafType);
        // upgrade unique_ptr to shared_ptr
        return std::shared_ptr<solidean::Mesh>(std::move(m));
    }

    // Recurse into children
    auto A = eval_to_mesh(ctx, arithmetic, *node.childA, leafType);
    auto B = eval_to_mesh(ctx, arithmetic, *node.childB, leafType);

    // Combine via a tiny operation that inputs A and B and outputs a new persistent mesh
    auto out = ctx.execute(arithmetic, [&](solidean::Operation& op)
    {
        auto a = op.input(*A);
        auto b = op.input(*B);

        switch (node.kind) {
            case CsgKind::Union:        return op.output(op.union_(a, b));
            case CsgKind::Difference:   return op.output(op.difference(a, b));
            case CsgKind::Intersection: return op.output(op.intersection(a, b));
            default:                    return op.output(a); // unreachable
        }
    });

    return std::shared_ptr<solidean::Mesh>(std::move(out));
}

// Example: final export after persistent evaluation
std::vector<example::triangle> evaluate_csg_persistent_and_export(solidean::Context& ctx,
                                                                  solidean::ExactArithmetic& arithmetic,
                                                                  CsgNode const& root,
                                                                  solidean::MeshType leafType = solidean::MeshType::Solid)
{
    auto rootMesh = eval_to_mesh(ctx, arithmetic, root, leafType);

    // NOTE: you could also return the rootMesh directly

    auto blob = ctx.execute(arithmetic, [&](solidean::Operation& op)
    {
        auto r = op.input(*rootMesh);
        return op.exportToTrianglesF32(r);
    });

    auto span = blob->getTrianglesF32<example::triangle>();
    return std::vector<example::triangle>(span.begin(), span.end());
}

Notes

Further tips

  • Variadic nodes: if your CSG nodes can have more than two children, simply fold the binary operator (union_, difference, intersection) across the children. Solidean can internally choose efficient evaluation orders.
  • Provenance: import leaves with IDs (…WithID) to keep track of which subtree contributed to which output primitive. Export WithID to populate PrimitiveIDs.
  • Quality flags: if leaves are not necessarily solid, import with MeshType::Supersolid or run a preprocessing pass (Operation::healOperation::selfUnion).