Fry3DX/src/boolean.cpp

465 lines
20 KiB
C++

// Combining shapes: one added to, taken from, or kept where it overlaps another. A BSP tree of
// each's faces cuts the other's (as csg.js does), then the pieces are joined up again: points
// shared, points that land on another piece's edge set into it (so it stays closed all round).
#include "mesh.h"
#include <manifold/manifold.h>
#include <algorithm>
#include <cmath>
#include <map>
#include <functional>
#include <memory>
#include <optional>
#include <set>
namespace fry {
namespace {
constexpr float kEps = 1e-6f;
struct Poly {
std::vector<glm::vec3> v;
glm::vec3 n{0.0f};
float w = 0.0f;
void plane() {
n = glm::normalize(glm::cross(v[1] - v[0], v[2] - v[0]));
w = glm::dot(n, v[0]);
}
void flip() {
std::reverse(v.begin(), v.end());
n = -n, w = -w;
}
};
// Splits `p` by the plane (n, w) into the four lists, as csg.js.
void split(const glm::vec3& n, float w, const Poly& p, std::vector<Poly>& coFront, std::vector<Poly>& coBack, std::vector<Poly>& front, std::vector<Poly>& back) {
enum { Co = 0, Front = 1, Back = 2, Spanning = 3 };
int kind = 0;
std::vector<int> types(p.v.size());
for (size_t i = 0; i < p.v.size(); ++i) {
const float t = glm::dot(n, p.v[i]) - w;
types[i] = t < -kEps ? Back : t > kEps ? Front : Co;
kind |= types[i];
}
switch (kind) {
case Co: (glm::dot(n, p.n) > 0 ? coFront : coBack).push_back(p); break;
case Front: front.push_back(p); break;
case Back: back.push_back(p); break;
default: {
Poly f, b;
for (size_t i = 0; i < p.v.size(); ++i) {
const size_t j = (i + 1) % p.v.size();
const int ti = types[i], tj = types[j];
const glm::vec3 vi = p.v[i], vj = p.v[j];
if (ti != Back) f.v.push_back(vi);
if (ti != Front) b.v.push_back(vi);
if ((ti | tj) == Spanning) {
const float t = (w - glm::dot(n, vi)) / glm::dot(n, vj - vi);
const glm::vec3 x = vi + (vj - vi) * t;
f.v.push_back(x), b.v.push_back(x);
}
}
f.n = b.n = p.n, f.w = b.w = p.w;
if (f.v.size() >= 3) front.push_back(f);
if (b.v.size() >= 3) back.push_back(b);
}
}
}
struct Node {
bool hasPlane = false;
glm::vec3 n{0.0f};
float w = 0.0f;
std::unique_ptr<Node> front, back;
std::vector<Poly> polys;
void invert() {
for (Poly& p : polys) p.flip();
n = -n, w = -w;
if (front) front->invert();
if (back) back->invert();
std::swap(front, back);
}
std::vector<Poly> clip(const std::vector<Poly>& in) const {
if (!hasPlane) return in;
std::vector<Poly> f, b;
for (const Poly& p : in) split(n, w, p, f, b, f, b);
if (front) f = front->clip(f);
if (back) b = back->clip(b);
else b.clear();
f.insert(f.end(), b.begin(), b.end());
return f;
}
void clipTo(const Node& other) {
polys = other.clip(polys);
if (front) front->clipTo(other);
if (back) back->clipTo(other);
}
void all(std::vector<Poly>& out) const {
out.insert(out.end(), polys.begin(), polys.end());
if (front) front->all(out);
if (back) back->all(out);
}
void build(const std::vector<Poly>& in, int depth = 0) {
if (in.empty()) return;
size_t first = 0;
if (!hasPlane) { // (the piece the plane is taken from stays here: a thin sliver's own corners can
// be off its rounded plane, and split by it again and again, forever)
hasPlane = true, n = in[0].n, w = in[0].w;
polys.push_back(in[0]);
first = 1;
}
if (depth > 3000) { // (a tree this deep is gone wrong: the rest kept here, not a stack overflow)
polys.insert(polys.end(), in.begin() + long(first), in.end());
return;
}
std::vector<Poly> f, b;
for (size_t i = first; i < in.size(); ++i) split(n, w, in[i], polys, polys, f, b);
if (!f.empty()) {
if (!front) front = std::make_unique<Node>();
front->build(f, depth + 1);
}
if (!b.empty()) {
if (!back) back = std::make_unique<Node>();
back->build(b, depth + 1);
}
}
};
std::vector<Poly> polysOf(const Mesh& m) {
std::vector<Poly> out;
for (size_t f = 0; f < m.faces.size(); ++f)
for (const auto& t : triangulate(m, int(f))) {
Poly p;
p.v = {m.verts[size_t(t[0])], m.verts[size_t(t[1])], m.verts[size_t(t[2])]};
if (glm::length(glm::cross(p.v[1] - p.v[0], p.v[2] - p.v[0])) < 1e-12f) continue;
p.plane();
out.push_back(p);
}
return out;
}
// The pieces as a mesh: points shared, and points that lie along another piece's edge set into it.
Mesh meshOf(const std::vector<Poly>& polys) {
Mesh m;
std::map<std::array<int64_t, 3>, int> at;
const float q = 1e5f; // (0.01 mm)
auto point = [&](const glm::vec3& p) {
const std::array<int64_t, 3> k{std::llround(p.x * q), std::llround(p.y * q), std::llround(p.z * q)};
auto [it, fresh] = at.try_emplace(k, int(m.verts.size()));
if (fresh) m.verts.push_back(p);
return it->second;
};
for (const Poly& p : polys) {
std::vector<int> f;
for (const glm::vec3& v : p.v) {
const int i = point(v);
if (f.empty() || f.back() != i) f.push_back(i);
}
while (f.size() > 1 && f.front() == f.back()) f.pop_back();
if (f.size() < 3) continue;
// A sliver squashed flat by the sharing of points (all its corners on one line): it
// would be a third face on an edge. Left out; its middle points are set into the
// neighbours' edges below, so it stays closed all round.
glm::vec3 area(0.0f);
for (size_t i = 0; i < f.size(); ++i) area += glm::cross(m.verts[size_t(f[i])], m.verts[size_t(f[(i + 1) % f.size()])]);
if (glm::length(area) < 2e-11f) continue; // (twice the area: about a square of 3 µm)
m.faces.push_back(f);
}
// T-junctions: a point in the middle of an edge it isn't a corner of.
std::vector<int> order(m.verts.size());
for (size_t i = 0; i < order.size(); ++i) order[i] = int(i);
std::sort(order.begin(), order.end(), [&](int a, int b) { return m.verts[size_t(a)].x < m.verts[size_t(b)].x; });
for (auto& f : m.faces) {
std::vector<int> out;
for (size_t i = 0; i < f.size(); ++i) {
const int a = f[i], b = f[(i + 1) % f.size()];
out.push_back(a);
const glm::vec3 pa = m.verts[size_t(a)], pb = m.verts[size_t(b)], d = pb - pa;
const float len2 = glm::dot(d, d);
if (len2 < 1e-14f) continue;
std::vector<std::pair<float, int>> on;
const float lo = std::min(pa.x, pb.x) - 2e-5f, hi = std::max(pa.x, pb.x) + 2e-5f;
auto it = std::lower_bound(order.begin(), order.end(), lo, [&](int v, float x) { return m.verts[size_t(v)].x < x; });
for (; it != order.end() && m.verts[size_t(*it)].x <= hi; ++it) {
const int v = *it;
if (v == a || v == b) continue;
const float t = glm::dot(m.verts[size_t(v)] - pa, d) / len2;
if (const float in = 2e-6f / std::sqrt(len2); t <= in || t >= 1 - in) continue; // (a µm or two from its ends: on a long edge, a part of it)
if (const glm::vec3 off = pa + d * t - m.verts[size_t(v)]; glm::dot(off, off) < 1e-10f) on.push_back({t, v});
}
std::sort(on.begin(), on.end());
for (auto [t, v] : on) out.push_back(v);
}
f = out;
}
return m;
}
// Faces that run out to a point and straight back (… a b a …): a needle along an edge, which
// would put a third face on it. The needle goes; the face is the same without it.
void removeSpikes(Mesh& m) {
for (auto& f : m.faces)
for (bool again = true; again && f.size() >= 3;) {
again = false;
for (size_t i = 0; i < f.size() && f.size() >= 3; ++i) {
const size_t n = f.size(), next = (i + 1) % n, after = (i + 2) % n;
if (f[i] == f[next]) { // (a point twice in a row)
f.erase(f.begin() + long(next));
again = true;
break;
}
if (f[i] == f[after]) { // a b a: b, and one a, go
const size_t lo = std::min(next, after), hi = std::max(next, after);
f.erase(f.begin() + long(hi)), f.erase(f.begin() + long(lo));
again = true;
break;
}
}
}
std::erase_if(m.faces, [](const std::vector<int>& f) { return f.size() < 3; });
}
// Gaps the cutting leaves (a tenth of a mm, where one side's rounding cut a piece the other
// didn't): an edge with no face back along it. A small loop of them, or a sliver of next to no
// area, gets a face of its own; the shape is closed again.
void closeSmallGaps(Mesh& m) {
// Two faces back to back on the same corners (a wall of no thickness): both go.
std::map<std::vector<int>, std::vector<size_t>> byCorners;
auto key = [](std::vector<int> f) {
std::rotate(f.begin(), std::min_element(f.begin(), f.end()), f.end());
return f;
};
for (size_t i = 0; i < m.faces.size(); ++i) byCorners[key(m.faces[i])].push_back(i);
std::vector<bool> drop(m.faces.size(), false);
for (size_t i = 0; i < m.faces.size(); ++i) {
if (drop[i]) continue;
std::vector<int> r(m.faces[i].rbegin(), m.faces[i].rend());
if (auto it = byCorners.find(key(r)); it != byCorners.end())
for (size_t j : it->second)
if (j != i && !drop[j]) {
drop[i] = drop[j] = true;
break;
}
}
for (size_t i = m.faces.size(); i-- > 0;)
if (drop[i]) m.faces.erase(m.faces.begin() + long(i));
std::set<std::pair<int, int>> have;
for (const auto& f : m.faces)
for (size_t i = 0; i < f.size(); ++i) have.insert({f[i], f[(i + 1) % f.size()]});
std::map<int, std::vector<int>> next; // (the open edges, a to b)
for (auto [a, b] : have)
if (!have.contains({b, a})) next[a].push_back(b);
auto fill = [&](std::vector<int> loop) {
if (loop.size() < 3) return;
float around = 0.0f;
glm::vec3 area(0.0f);
for (size_t i = 0; i < loop.size(); ++i) {
const glm::vec3 p = m.verts[size_t(loop[i])], q = m.verts[size_t(loop[(i + 1) % loop.size()])];
around += glm::distance(p, q), area += glm::cross(p, q);
}
// A small one, or one next to flat: a piece the cutting lost. Else a real opening: left.
const glm::vec3 n = glm::length(area) > 0.0f ? glm::normalize(area) : glm::vec3(0.0f);
float bulge = 0.0f;
for (int v : loop) bulge = std::max(bulge, std::abs(glm::dot(m.verts[size_t(v)] - m.verts[size_t(loop[0])], n)));
if (around > 2e-3f && glm::length(area) > 2e-9f && bulge > 1e-4f) return;
std::reverse(loop.begin(), loop.end());
m.faces.push_back(loop);
};
while (!next.empty()) {
std::vector<int> loop{next.begin()->first};
while (!loop.empty() && loop.size() <= 64) {
auto it = next.find(loop.back());
if (it == next.end()) break; // (an open run, not a loop)
const int b = it->second.back();
it->second.pop_back();
if (it->second.empty()) next.erase(it);
// Back at a point already passed: the loop from there closes (two gaps meeting at a corner, a figure 8).
if (auto at = std::find(loop.begin(), loop.end(), b); at != loop.end()) {
fill(std::vector<int>(at, loop.end()));
loop.erase(at + 1, loop.end());
if (loop.size() == 1 && !next.contains(loop[0])) break;
} else
loop.push_back(b);
}
}
}
// Every edge has as many faces along it one way as the other (closed; edges where two parts
// only touch count too).
bool sealed(const Mesh& m) {
std::map<std::pair<int, int>, int> e;
for (const auto& f : m.faces)
for (size_t i = 0; i < f.size(); ++i) {
const int a = f[i], b = f[(i + 1) % f.size()];
a < b ? ++e[{a, b}] : --e[{b, a}];
}
for (auto [k, n] : e)
if (n) return false;
return true;
}
Mesh combineOnce(const Mesh& am, const Mesh& b, Combine op) {
Node A, B;
A.build(polysOf(am));
B.build(polysOf(b));
if (!A.hasPlane || !B.hasPlane) return {};
std::vector<Poly> bp;
switch (op) {
case Combine::Add:
A.clipTo(B), B.clipTo(A), B.invert(), B.clipTo(A), B.invert();
B.all(bp), A.build(bp);
break;
case Combine::Subtract:
A.invert(), A.clipTo(B), B.clipTo(A), B.invert(), B.clipTo(A), B.invert();
B.all(bp), A.build(bp), A.invert();
break;
case Combine::Overlap:
A.invert(), B.clipTo(A), B.invert(), A.clipTo(B), B.clipTo(A);
B.all(bp), A.build(bp), A.invert();
break;
}
std::vector<Poly> out;
A.all(out);
Mesh a = meshOf(out);
// Points the cutting left a hair apart (closer than 0.03 mm: nothing a printer or a model
// could show) are one; else they fold tiny faces back over their neighbours.
mergeByDistance(a, 3e-5f);
removeSpikes(a);
mergeFlatFaces(a);
removeSpikes(a);
closeSmallGaps(a);
return a;
}
// Manifold's way, for two closed shapes: exact, and doesn't go wrong where faces just touch.
// Each triangle carries the face it came from, so its faces come back whole (not triangles).
// Nothing if either isn't closed all round the way it needs (then the BSP's way).
std::optional<Mesh> combineManifold(const Mesh& a, const Mesh& b, Combine op) {
auto toManifold = [](const Mesh& m, uint32_t firstFace) -> std::optional<manifold::Manifold> {
manifold::MeshGL g;
g.numProp = 3;
g.vertProperties.reserve(m.verts.size() * 3);
for (const glm::vec3& v : m.verts) {
if (!std::isfinite(v.x) || !std::isfinite(v.y) || !std::isfinite(v.z)) return std::nullopt; // (it can't take those)
g.vertProperties.insert(g.vertProperties.end(), {v.x, v.y, v.z});
}
for (size_t f = 0; f < m.faces.size(); ++f) {
const auto& fv = m.faces[f];
if (fv.size() < 3) return std::nullopt; // (a line: not a solid)
for (size_t i = 0; i < fv.size(); ++i) // (a point twice round a face, or one that isn't there: it can't take that either)
if (fv[i] < 0 || size_t(fv[i]) >= m.verts.size() || std::find(fv.begin() + long(i) + 1, fv.end(), fv[i]) != fv.end()) return std::nullopt;
for (const auto& t : triangulate(m, int(f)))
if (t[0] == t[1] || t[1] == t[2] || t[0] == t[2]) return std::nullopt;
else g.triVerts.insert(g.triVerts.end(), {uint32_t(t[0]), uint32_t(t[1]), uint32_t(t[2])}), g.faceID.push_back(firstFace + uint32_t(f));
}
if (g.triVerts.empty()) return std::nullopt;
g.runOriginalID = {manifold::Manifold::ReserveIDs(1)}; // (so it keeps the faces given, not its own numbering)
g.runIndex = {0, uint32_t(g.triVerts.size())};
g.Merge(); // (points in the same place, kept apart by a seam: one)
manifold::Manifold out(g);
if (out.Status() != manifold::Manifold::Error::NoError) return std::nullopt;
return out;
};
const auto A = toManifold(a, 0), B = toManifold(b, uint32_t(a.faces.size()));
if (!A || !B) return std::nullopt;
const manifold::Manifold r = op == Combine::Add ? *A + *B : op == Combine::Subtract ? *A - *B : *A ^ *B;
if (r.Status() != manifold::Manifold::Error::NoError) return std::nullopt;
const manifold::MeshGL g = r.GetMeshGL();
Mesh out;
const size_t n = g.NumVert();
out.verts.reserve(n);
for (size_t i = 0; i < n; ++i) out.verts.push_back({g.vertProperties[i * g.numProp], g.vertProperties[i * g.numProp + 1], g.vertProperties[i * g.numProp + 2]});
// The triangles of each face, joined back into it: its outline, walked round (a face cut into
// two pieces, or with a hole in it: as it comes, triangles).
// A face of one shape lying flat against a face of the other, side by side (both cubes' tops
// in a row): one face. A shape's own faces stay as they were.
const size_t tris = g.triVerts.size() / 3;
const uint32_t split = uint32_t(a.faces.size());
std::vector<uint32_t> id(tris);
for (size_t t = 0; t < tris; ++t) id[t] = t < g.faceID.size() ? g.faceID[t] : uint32_t(0x80000000u + t);
std::map<uint32_t, uint32_t> parent;
std::function<uint32_t(uint32_t)> root = [&](uint32_t x) {
auto it = parent.find(x);
if (it == parent.end() || it->second == x) return x;
return it->second = root(it->second);
};
std::map<uint32_t, glm::vec4> plane; // (each face's: its normal and how far along it)
for (size_t t = 0; t < tris; ++t) {
if (plane.count(id[t])) continue;
const glm::vec3 p0 = out.verts[g.triVerts[t * 3]], p1 = out.verts[g.triVerts[t * 3 + 1]], p2 = out.verts[g.triVerts[t * 3 + 2]];
const glm::vec3 c = glm::cross(p1 - p0, p2 - p0);
if (glm::length(c) > 1e-12f) plane[id[t]] = glm::vec4(glm::normalize(c), glm::dot(glm::normalize(c), p0));
}
float size = 0;
for (const glm::vec3& v : out.verts) size = std::max(size, std::max(std::abs(v.x), std::max(std::abs(v.y), std::abs(v.z))));
std::map<std::pair<int, int>, size_t> edgeTri;
for (size_t t = 0; t < tris; ++t)
for (int k = 0; k < 3; ++k) edgeTri[{int(g.triVerts[t * 3 + size_t(k)]), int(g.triVerts[t * 3 + size_t((k + 1) % 3)])}] = t;
for (const auto& [e, t] : edgeTri) {
auto o = edgeTri.find({e.second, e.first});
if (o == edgeTri.end()) continue;
const uint32_t x = id[t], y = id[o->second];
if (x == y || (x < split) == (y < split) || !plane.count(x) || !plane.count(y)) continue;
const glm::vec4 px = plane[x], py = plane[y];
if (glm::dot(glm::vec3(px), glm::vec3(py)) > 1 - 1e-5f && std::abs(px.w - py.w) < 1e-5f * std::max(size, 1.0f)) {
const uint32_t rx = root(x), ry = root(y);
if (rx != ry) parent[rx] = ry, parent.emplace(ry, ry);
}
}
std::map<uint32_t, std::vector<std::array<int, 3>>> faces;
for (size_t t = 0; t < tris; ++t) faces[root(id[t])].push_back({int(g.triVerts[t * 3]), int(g.triVerts[t * 3 + 1]), int(g.triVerts[t * 3 + 2])});
for (const auto& [id, ts] : faces) {
std::set<std::pair<int, int>> inside;
for (const auto& t : ts)
for (int k = 0; k < 3; ++k) inside.insert({t[size_t(k)], t[size_t((k + 1) % 3)]});
std::map<int, int> next;
bool simple = true;
for (const auto& [p, q] : inside)
if (!inside.count({q, p})) simple &= next.emplace(p, q).second; // (an outline edge)
std::vector<int> loop;
if (simple && !next.empty()) {
int at = next.begin()->first;
do {
loop.push_back(at);
auto it = next.find(at);
if (it == next.end() || loop.size() > next.size()) break;
at = it->second;
} while (at != loop.front());
}
if (simple && loop.size() == next.size() && loop.size() >= 3) out.faces.push_back(loop);
else
for (const auto& t : ts) out.faces.push_back({t[0], t[1], t[2]});
}
return out;
}
} // namespace
bool combine(Mesh& a, const Mesh& b, Combine op) {
if (auto m = combineManifold(a, b, op)) {
a = std::move(*m);
return !a.faces.empty();
}
Mesh out = combineOnce(a, b, op);
// Closed going in but not coming out: the rounding went wrong somewhere. Again with the
// other shape moved a few µm (nothing that shows), and the first that comes out closed kept.
if (!out.faces.empty() && !sealed(out) && sealed(a) && sealed(b)) {
static const glm::vec3 nudge[] = {{3e-6f, 5e-6f, 7e-6f}, {-6e-6f, 4e-6f, -3e-6f}, {5e-6f, -7e-6f, 4e-6f}};
for (const glm::vec3& d : nudge) {
Mesh moved = b;
for (glm::vec3& v : moved.verts) v += d;
if (Mesh again = combineOnce(a, moved, op); !again.faces.empty() && sealed(again)) {
out = std::move(again);
break;
}
}
}
a = std::move(out);
return !a.faces.empty();
}
} // namespace fry