Iterated Booleans (Milling Demo)

Subtract a moving tool from a workpiece along a path; chain operations with input/output and measure performance.

The goal of this tutorial is to simulate a simple milling workflow with iterated Booleans. We will:

  1. Create a cube “workpiece” and a small sphere “tool”
  2. Generate a circular tool path (many steps)
  3. For each step, subtract the tool from the current workpiece
  4. Pass results between iterations using Operation::input / Operation::output
  5. Export the final result and measure timing

Along the way, you’ll use: Context, ExactArithmetic, Operation::input, Operation::difference, Operation::output, Operation::exportToTrianglesF32, and ExecuteMode.

Note:
All C++ tutorials/examples use ExampleFramework.hh to provide tiny POD types (pos3, triangle) and helpers.

1) Project setup (CMake)

Add the C++17 binding and link it:

add_subdirectory(path/to/solidean/lang/cpp17)
target_link_libraries(YourProject PRIVATE Solidean::Cpp17)

Ensure the Solidean dynamic library is discoverable at runtime (same folder as your binary or on your system path).

2) Minimal program

We iteratively subtract a translated sphere from a cube along a circular path, timing only the Boolean loop.

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

#include <solidean.hh>
#include "ExampleFramework.hh" // pos3, triangle, createCube, createIcoSphere

int main() {
    // 1) Context + exact arithmetic (bounding box [-10,+10]^3)
    auto ctx = solidean::Context::create();
    auto arithmetic = ctx->createExactArithmetic(10.0f);

    // 2) Workpiece: unit cube centered at origin (returns std::vector<example::triangle>)
    auto workpieceTris = example::createCube();

    // 3) Tool prototype: small icosphere at origin, radius 0.15
    const float toolRadius = 0.15f;
    auto toolPrototype = example::createIcoSphere(/*subdiv*/3, /*center*/{0.0f, 0.0f, 0.0f}, toolRadius);

    // Create persistent mesh for the workpiece
    auto workpiece = ctx->createMeshFromTrianglesF32(
        solidean::as_triangle3_span(workpieceTris), *arithmetic);

    // 4) Build a circular tool path (XZ circle at y=+0.5)
    std::vector<example::pos3> path;
    const int   steps = 50;
    const float radius = 0.35f;
    for (int i = 0; i < steps; ++i) {
        float t = float(i) / float(steps);
        float x = radius * std::cos(2.0f * float(M_PI) * t);
        float z = radius * std::sin(2.0f * float(M_PI) * t);
        path.push_back({x, 0.5f, z});
    }

    std::cout << "Iteratively subtracting tool along a circular path (" << steps << " steps)..." << std::endl;

    // 5) Iterate: translate tool → subtract from current workpiece → materialize new workpiece
    auto t0 = std::chrono::high_resolution_clock::now();

    for (int i = 0; i < steps; ++i) {
        // Translate the tool prototype to the current path position
        std::vector<example::triangle> toolStep = toolPrototype;
        for (auto& tri : toolStep) {
            tri.p0 += path[i];
            tri.p1 += path[i];
            tri.p2 += path[i];
        }

        // Execute one subtraction step and materialize the new workpiece
        workpiece = ctx->execute( //
            *arithmetic,
            [&](solidean::Operation& op) {
                auto in  = op.input(*workpiece);
                auto cut = op.importFromTrianglesF32(
                    solidean::as_triangle3_span(toolStep),
                    solidean::MeshType::Supersolid); // conservative guarantee
                return op.output(op.difference(in, cut));
            });
        std::cout << "." << std::flush;
    }
    std::cout << std::endl;

    auto t1 = std::chrono::high_resolution_clock::now();
    double ms = std::chrono::duration<double, std::milli>(t1 - t0).count();
    std::cout << "Finished " << steps << " iterations in " << ms
            << " ms (~" << (ms / steps) << " ms/step)" << std::endl;

    // 6) Export final result as unrolled triangles (use indexed if you need connectivity)
    auto blob = ctx->execute(*arithmetic, [&](solidean::Operation& op) {
        auto m = op.input(*workpiece);
        return op.exportToTrianglesF32(m);
    });

    auto triSpan = blob->getTrianglesF32<example::triangle>();
    std::cout << "Final triangle count: " << triSpan.size() << std::endl;

    return EXIT_SUCCESS;
}

3) What you learned

  • Chaining via persistent meshes:
    Use Operation::output to materialize a persistent Mesh at the end of each step, then feed it back with Operation::input in the next iteration.
    This is the correct way to carry results across operations (do not reuse MeshOperand objects across different operations).

  • Record–then–execute with a lambda:
    The C++17 helper calls Context::execute with a lambda that records the step (input → boolean → output) and returns the final handle/export.

  • Output-sensitive performance:
    Iterated Booleans are designed to scale with what changes each step rather than full recomputation, yielding predictable iteration times.

  • Geometry guarantees:
    If a tool might self-intersect, import it as MeshType::Supersolid to declare the right guarantees.

  • Export for visualization or downstream processing:
    Use Operation::exportToTrianglesF32 for a quick, unrolled preview. For connectivity and size, prefer indexed exports.

4) Variations to try

  • Swap the path for a lawnmower or spiral pattern and compare iteration timing.
  • Export indexed triangles with Operation::exportToIndexedTrianglesF32 and compute connected components.
  • Enable ExecuteMode::Debug to surface input-constraint violations (slower, but informative).
  • Replace the sphere with a more complex tool and use selfUnion first to make it strictly solid before iterating.

Next steps