This guide shows two pragmatic ways to evaluate a CSG tree into a mesh:
- A) Single-shot: build one
Operationthat imports all leaves and records the whole Boolean expression, then export once. - B) Iterated/persistent: recursively materialize subtrees as persistent
Meshobjects 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
- Leafs are imported via
Operation::importFromTrianglesF32. - Internal nodes use
Operation::union,Operation::difference,Operation::intersection. - You can export indexed topology instead using
Operation::exportToIndexedTrianglesF32, or use the generalOperation::exportMeshwithExportFormatandExportOption. - If you want provenance per subtree, import leaves with distinct IDs via
Operation::importFromTrianglesF32WithIDand export WithID to populateDataSlot::PrimitiveIDs.
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
- Persistent materialization uses
Operation::input→ Boolean →Operation::outputper internal node. - This pattern is great for caching the meshes of subtrees (or CSG graphs) and reusing them in different contexts.
- For connectivity/topology, prefer an indexed export from the final step via
Operation::exportToIndexedTrianglesF32.
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 populatePrimitiveIDs. - Quality flags: if leaves are not necessarily solid, import with
MeshType::Supersolidor run a preprocessing pass (Operation::heal→Operation::selfUnion).