Combining: points the cutting leaves a hair apart are one, and needles along edges go — the example gear (and others like it) came out with an edge of three faces and a face with no size, so not ready to print; a test for it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Nexoniarz 2026-10-20 17:57:13 +02:00
commit 5d3121b335
2 changed files with 55 additions and 1 deletions

View file

@ -143,7 +143,14 @@ Mesh meshOf(const std::vector<Poly>& polys) {
if (f.empty() || f.back() != i) f.push_back(i); if (f.empty() || f.back() != i) f.push_back(i);
} }
while (f.size() > 1 && f.front() == f.back()) f.pop_back(); 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. // T-junctions: a point in the middle of an edge it isn't a corner of.
std::vector<int> order(m.verts.size()); std::vector<int> order(m.verts.size());
@ -175,6 +182,30 @@ Mesh meshOf(const std::vector<Poly>& polys) {
return m; 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; });
}
} // namespace } // namespace
bool combine(Mesh& a, const Mesh& b, Combine op) { bool combine(Mesh& a, const Mesh& b, Combine op) {
@ -200,7 +231,12 @@ bool combine(Mesh& a, const Mesh& b, Combine op) {
std::vector<Poly> out; std::vector<Poly> out;
A.all(out); A.all(out);
a = meshOf(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); mergeFlatFaces(a);
removeSpikes(a);
return !a.faces.empty(); return !a.faces.empty();
} }

View file

@ -426,6 +426,24 @@ int main() {
const std::string text((std::istreambuf_iterator<char>(j)), {}); const std::string text((std::istreambuf_iterator<char>(j)), {});
CHECK(text.find("\"translation\":[2,3,1]") != std::string::npos, "in glTF's axes (Y up): the position is [2,3,1]"); 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<glm::vec3> 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. { // Sculpting: a cube made finer keeps its shape; brushes move only what's under them.
Mesh m = makeCube(1.0f); Mesh m = makeCube(1.0f);
const int times = finerForSculpting(m, 2000); const int times = finerForSculpting(m, 2000);