diff --git a/src/boolean.cpp b/src/boolean.cpp index d2253ad..49f19f6 100644 --- a/src/boolean.cpp +++ b/src/boolean.cpp @@ -143,7 +143,14 @@ Mesh meshOf(const std::vector& polys) { 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) m.faces.push_back(f); + 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 order(m.verts.size()); @@ -175,6 +182,30 @@ Mesh meshOf(const std::vector& polys) { 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& f) { return f.size() < 3; }); +} + } // namespace bool combine(Mesh& a, const Mesh& b, Combine op) { @@ -200,7 +231,12 @@ bool combine(Mesh& a, const Mesh& b, Combine op) { std::vector out; A.all(out); 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); return !a.faces.empty(); } diff --git a/tests/mesh_test.cpp b/tests/mesh_test.cpp index df11c09..0225263 100644 --- a/tests/mesh_test.cpp +++ b/tests/mesh_test.cpp @@ -426,6 +426,24 @@ int main() { const std::string text((std::istreambuf_iterator(j)), {}); CHECK(text.find("\"translation\":[2,3,1]") != std::string::npos, "in glTF's axes (Y up): the position is [2,3,1]"); + { // A gear with a screw hole (scripts/gear.lua): cutting leaves points a hair apart and + // needles along edges; what comes out must still print (closed, nothing without size). + for (int teeth : {10, 16}) + for (int sides : {24, 32}) { + std::vector o; + const float radius = teeth == 16 ? 0.03f : 0.018f; + for (int i = 0; i < teeth * 4; ++i) { + const float a = float(i) / float(teeth * 4) * 6.2831853f, r = i % 4 < 2 ? radius + 0.004f : radius; + o.push_back({std::cos(a) * r, std::sin(a) * r, 0}); + } + Mesh g = extrudeOutline(o, {0, 0, 1}, 0.008f); + transform(g, glm::translate(glm::mat4(1.0f), glm::vec3(0, 0, 0.004f))); + combine(g, makeCylinder(sides, 0.0027f, 0.03f), Combine::Subtract); + const PrintCheck c = checkForPrint(g); + CHECK(c.closed() && c.tinyFaces == 0 && closed(g), "a %d-tooth gear minus a %d-sided hole prints (open %d, crowded %d, flipped %d, tiny %d)", teeth, sides, + c.openEdges, c.crowdedEdges, c.flippedEdges, c.tinyFaces); + } + } { // Sculpting: a cube made finer keeps its shape; brushes move only what's under them. Mesh m = makeCube(1.0f); const int times = finerForSculpting(m, 2000);