mirror of
https://github.com/Nexoniarz/Fry3DX.git
synced 2026-11-03 12:34:27 +00:00
Repair for printing (joins loose corners, drops empty and doubled faces and inside walls, turns faces out, closes holes): a button in Print and a command; joining points by a grid (an imported STL's 100k corners: milliseconds, not minutes); tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
1a09c17155
commit
f5d1b79756
5 changed files with 159 additions and 7 deletions
37
src/app.cpp
37
src/app.cpp
|
|
@ -3334,6 +3334,12 @@ private:
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::SeparatorText("Ready to print?");
|
ImGui::SeparatorText("Ready to print?");
|
||||||
|
if (!all.closed() || all.insideOut || all.tinyFaces) {
|
||||||
|
pushAccent();
|
||||||
|
if (ImGui::Button("Repair it (all at once)", ImVec2(w, 0))) repairSelected();
|
||||||
|
popAccent();
|
||||||
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Joins loose corners, takes away empty and doubled faces, turns faces outwards, closes holes");
|
||||||
|
}
|
||||||
if (all.openEdges) {
|
if (all.openEdges) {
|
||||||
bad(std::format("Not closed: {} edges have a face on one side only (holes). A slicer may fill them wrongly or not print it.", all.openEdges));
|
bad(std::format("Not closed: {} edges have a face on one side only (holes). A slicer may fill them wrongly or not print it.", all.openEdges));
|
||||||
if (ImGui::Button("Close the holes", ImVec2(w, 0))) {
|
if (ImGui::Button("Close the holes", ImVec2(w, 0))) {
|
||||||
|
|
@ -7824,7 +7830,36 @@ private:
|
||||||
|
|
||||||
// ── (being made) ──
|
// ── (being made) ──
|
||||||
bool showPrintSize_ = false, liveMirror_ = false;
|
bool showPrintSize_ = false, liveMirror_ = false;
|
||||||
void repairSelected() {}
|
// Each picked object (or all, in the Print tab with none picked) made ready to print.
|
||||||
|
void repairSelected() {
|
||||||
|
std::vector<Object*> targets;
|
||||||
|
if (tab_ == Tab::Print) targets = printTargets();
|
||||||
|
else
|
||||||
|
for (Object& o : scene_.objects)
|
||||||
|
if (selected_.count(o.id)) targets.push_back(&o);
|
||||||
|
std::erase_if(targets, [](const Object* o) { return o->locked; });
|
||||||
|
if (targets.empty()) return say("Pick what to repair first");
|
||||||
|
if (mode_ == Mode::Edit) leaveEdit();
|
||||||
|
pushUndo();
|
||||||
|
fry::Repair all;
|
||||||
|
int still = 0;
|
||||||
|
for (Object* o : targets) {
|
||||||
|
const fry::Repair r = fry::repairForPrint(o->mesh);
|
||||||
|
all.joinedPoints += r.joinedPoints, all.emptyFaces += r.emptyFaces, all.doubledFaces += r.doubledFaces, all.turnedFaces += r.turnedFaces, all.closedHoles += r.closedHoles;
|
||||||
|
if (r.total()) handMade(*o), touched(o->id);
|
||||||
|
still += !checkForPrint(o->mesh).closed();
|
||||||
|
}
|
||||||
|
if (!all.total()) return takeBack(), say(still ? "Nothing it can repair by itself: look at what the Print tab says" : "Nothing to repair: ready to print");
|
||||||
|
std::vector<std::string> did;
|
||||||
|
if (all.joinedPoints) did.push_back(std::format("{} points joined", all.joinedPoints));
|
||||||
|
if (all.emptyFaces) did.push_back(std::format("{} empty faces gone", all.emptyFaces));
|
||||||
|
if (all.doubledFaces) did.push_back(std::format("{} doubled faces gone", all.doubledFaces));
|
||||||
|
if (all.turnedFaces) did.push_back(std::format("{} faces turned out", all.turnedFaces));
|
||||||
|
if (all.closedHoles) did.push_back(std::format("{} holes closed", all.closedHoles));
|
||||||
|
std::string text;
|
||||||
|
for (const std::string& d : did) text += (text.empty() ? "" : ", ") + d;
|
||||||
|
say("Repaired: " + text + (still ? std::format(". {} still not closed (see the Print tab)", still) : ". Ready to print"));
|
||||||
|
}
|
||||||
void startSlide() {}
|
void startSlide() {}
|
||||||
|
|
||||||
std::string workspaceDir() const { return keel::prefPath() + "workspaces/"; }
|
std::string workspaceDir() const { return keel::prefPath() + "workspaces/"; }
|
||||||
|
|
|
||||||
24
src/mesh.cpp
24
src/mesh.cpp
|
|
@ -4,6 +4,7 @@
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <numeric>
|
#include <numeric>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
namespace fry {
|
namespace fry {
|
||||||
|
|
||||||
|
|
@ -376,13 +377,24 @@ bool makeFace(Mesh& m, const std::vector<int>& verts, const glm::vec3& towards)
|
||||||
int mergeByDistance(Mesh& m, float distance) {
|
int mergeByDistance(Mesh& m, float distance) {
|
||||||
std::vector<int> to(m.verts.size(), -1);
|
std::vector<int> to(m.verts.size(), -1);
|
||||||
std::vector<glm::vec3> kept;
|
std::vector<glm::vec3> kept;
|
||||||
|
// (kept points in a grid of cubes `distance` big: one is looked for in its own and the 26
|
||||||
|
// round it, not among all of them; an imported STL has hundreds of thousands)
|
||||||
|
const float cell = std::max(distance, 1e-9f);
|
||||||
|
auto cellOf = [&](const glm::vec3& p) { return glm::ivec3(glm::floor(p / cell)); };
|
||||||
|
auto key = [](glm::ivec3 c) { return (uint64_t(uint32_t(c.x) & 0x1FFFFF) << 42) | (uint64_t(uint32_t(c.y) & 0x1FFFFF) << 21) | uint64_t(uint32_t(c.z) & 0x1FFFFF); };
|
||||||
|
std::unordered_map<uint64_t, std::vector<int>> grid;
|
||||||
|
grid.reserve(m.verts.size());
|
||||||
for (size_t v = 0; v < m.verts.size(); ++v) {
|
for (size_t v = 0; v < m.verts.size(); ++v) {
|
||||||
for (size_t k = 0; k < kept.size(); ++k)
|
const glm::ivec3 c = cellOf(m.verts[v]);
|
||||||
if (glm::distance(kept[k], m.verts[v]) <= distance) {
|
int best = -1;
|
||||||
to[v] = int(k);
|
for (int dx = -1; dx <= 1; ++dx)
|
||||||
break;
|
for (int dy = -1; dy <= 1; ++dy)
|
||||||
}
|
for (int dz = -1; dz <= 1; ++dz)
|
||||||
if (to[v] < 0) to[v] = int(kept.size()), kept.push_back(m.verts[v]);
|
if (auto it = grid.find(key(c + glm::ivec3(dx, dy, dz))); it != grid.end())
|
||||||
|
for (int k : it->second)
|
||||||
|
if ((best < 0 || k < best) && glm::distance(kept[size_t(k)], m.verts[v]) <= distance) best = k; // (the first kept, as before)
|
||||||
|
if (best < 0) best = int(kept.size()), kept.push_back(m.verts[v]), grid[key(c)].push_back(best);
|
||||||
|
to[v] = best;
|
||||||
}
|
}
|
||||||
const int gone = int(m.verts.size() - kept.size());
|
const int gone = int(m.verts.size() - kept.size());
|
||||||
remapFaces(m, to);
|
remapFaces(m, to);
|
||||||
|
|
|
||||||
|
|
@ -178,6 +178,14 @@ bool overhangs(const Mesh& m, int face, float overhang, float bedZ);
|
||||||
int closeHoles(Mesh& m);
|
int closeHoles(Mesh& m);
|
||||||
// Turns faces so each piece's all face the same way, outwards; how many were turned.
|
// Turns faces so each piece's all face the same way, outwards; how many were turned.
|
||||||
int faceOutward(Mesh& m);
|
int faceOutward(Mesh& m);
|
||||||
|
// Made ready to print, as far as it can be without guessing: points in the same place joined (an
|
||||||
|
// STL's corners come apart), faces with no area gone, faces there twice once (back to back: both
|
||||||
|
// gone, an inside wall), faces turned the same way, holes closed. What it did.
|
||||||
|
struct Repair {
|
||||||
|
int joinedPoints = 0, emptyFaces = 0, doubledFaces = 0, turnedFaces = 0, closedHoles = 0;
|
||||||
|
int total() const { return joinedPoints + emptyFaces + doubledFaces + turnedFaces + closedHoles; }
|
||||||
|
};
|
||||||
|
Repair repairForPrint(Mesh& m);
|
||||||
|
|
||||||
// ── Its picture's layout ──
|
// ── Its picture's layout ──
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -119,4 +119,60 @@ int faceOutward(Mesh& m) {
|
||||||
return turned;
|
return turned;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Repair repairForPrint(Mesh& m) {
|
||||||
|
Repair r;
|
||||||
|
if (m.verts.empty()) return r;
|
||||||
|
// (how near is "the same place": a millionth of its size, a micron at least)
|
||||||
|
const float size = glm::length(boundsMax(m) - boundsMin(m));
|
||||||
|
const size_t before = m.verts.size(), faces = m.faces.size();
|
||||||
|
mergeByDistance(m, std::max(size * 1e-6f, 1e-6f)); // (it drops faces left with fewer than three points: counted below)
|
||||||
|
r.joinedPoints = int(before - m.verts.size());
|
||||||
|
// Faces with a point twice in a row, fewer than three points, or no area.
|
||||||
|
const float tiny = std::max(size * size * 1e-12f, 1e-16f);
|
||||||
|
for (auto& f : m.faces) {
|
||||||
|
f.erase(std::unique(f.begin(), f.end()), f.end());
|
||||||
|
while (f.size() > 1 && f.front() == f.back()) f.pop_back();
|
||||||
|
}
|
||||||
|
std::erase_if(m.faces, [&](const std::vector<int>& f) { return f.size() < 3; });
|
||||||
|
for (size_t i = m.faces.size(); i-- > 0;)
|
||||||
|
if (faceArea(m, int(i)) <= tiny) m.faces.erase(m.faces.begin() + std::ptrdiff_t(i));
|
||||||
|
r.emptyFaces = int(faces - m.faces.size());
|
||||||
|
// The same face twice: once. Back to back (the same points, the other way round): neither.
|
||||||
|
std::map<std::vector<int>, std::vector<size_t>> same;
|
||||||
|
for (size_t i = 0; i < m.faces.size(); ++i) {
|
||||||
|
std::vector<int> key = m.faces[i];
|
||||||
|
std::sort(key.begin(), key.end());
|
||||||
|
same[key].push_back(i);
|
||||||
|
}
|
||||||
|
std::vector<char> drop(m.faces.size(), 0);
|
||||||
|
auto turn = [](const std::vector<int>& f) { // (the face started at its lowest point: two that turn the same way match)
|
||||||
|
std::vector<int> g = f;
|
||||||
|
std::rotate(g.begin(), std::min_element(g.begin(), g.end()), g.end());
|
||||||
|
return g;
|
||||||
|
};
|
||||||
|
for (const auto& [key, list] : same) {
|
||||||
|
if (list.size() < 2) continue;
|
||||||
|
int ways = 0, other = 0;
|
||||||
|
const std::vector<int> first = turn(m.faces[list[0]]);
|
||||||
|
for (size_t i : list) (turn(m.faces[i]) == first ? ways : other)++;
|
||||||
|
if (ways == other) // (back to back: an inside wall)
|
||||||
|
for (size_t i : list) drop[i] = 1;
|
||||||
|
else
|
||||||
|
for (size_t k = 1; k < list.size(); ++k) drop[list[k]] = 1;
|
||||||
|
}
|
||||||
|
size_t at = 0;
|
||||||
|
for (size_t i = 0; i < m.faces.size(); ++i)
|
||||||
|
if (!drop[i]) {
|
||||||
|
if (at != i) m.faces[at] = std::move(m.faces[i]); // (not onto itself: that empties it)
|
||||||
|
++at;
|
||||||
|
}
|
||||||
|
r.doubledFaces = int(m.faces.size() - at);
|
||||||
|
m.faces.resize(at);
|
||||||
|
removeUnused(m);
|
||||||
|
r.turnedFaces = faceOutward(m);
|
||||||
|
r.closedHoles = closeHoles(m);
|
||||||
|
if (r.closedHoles) r.turnedFaces += faceOutward(m);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace fry
|
} // namespace fry
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
#include <glm/gtc/matrix_transform.hpp>
|
#include <glm/gtc/matrix_transform.hpp>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
|
@ -365,6 +366,46 @@ int main() {
|
||||||
CHECK(combine(a, drill, Combine::Subtract) && checkForPrint(a).closed() && checkForPrint(a).volume < 1.0f && checkForPrint(a).volume > 0.85f,
|
CHECK(combine(a, drill, Combine::Subtract) && checkForPrint(a).closed() && checkForPrint(a).volume < 1.0f && checkForPrint(a).volume > 0.85f,
|
||||||
"a cylinder drilled through a cube: closed, %g m3", checkForPrint(a).volume);
|
"a cylinder drilled through a cube: closed, %g m3", checkForPrint(a).volume);
|
||||||
}
|
}
|
||||||
|
// Joining points is quick on a big mesh: a sphere in loose triangles, as an STL brings it.
|
||||||
|
{
|
||||||
|
const Mesh sphere = makeSphere(200, 100, 1.0f);
|
||||||
|
Mesh soup;
|
||||||
|
for (size_t f = 0; f < sphere.faces.size(); ++f)
|
||||||
|
for (const auto& t : triangulate(sphere, int(f))) {
|
||||||
|
soup.faces.push_back({int(soup.verts.size()), int(soup.verts.size()) + 1, int(soup.verts.size()) + 2});
|
||||||
|
for (int v : t) soup.verts.push_back(sphere.verts[size_t(v)]);
|
||||||
|
}
|
||||||
|
const size_t loose = soup.verts.size();
|
||||||
|
const auto t0 = std::chrono::steady_clock::now();
|
||||||
|
mergeByDistance(soup, 1e-6f);
|
||||||
|
const double ms = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - t0).count();
|
||||||
|
CHECK(soup.verts.size() == sphere.verts.size() && ms < 3000, "%zu loose points joined to %zu in %.0f ms", loose, soup.verts.size(), ms);
|
||||||
|
}
|
||||||
|
// Repair: a cube as an STL brings it (each face its own corners), one face missing, one
|
||||||
|
// turned in, one there twice, an inside wall: closed and the right way out after.
|
||||||
|
{
|
||||||
|
const Mesh good = makeCube(1.0f);
|
||||||
|
Mesh bad;
|
||||||
|
for (size_t f = 0; f < good.faces.size(); ++f) {
|
||||||
|
if (f == 2) continue; // (missing)
|
||||||
|
std::vector<int> face;
|
||||||
|
for (int v : good.faces[f]) face.push_back(int(bad.verts.size())), bad.verts.push_back(good.verts[size_t(v)] + glm::vec3(f == 4 ? 1e-8f : 0.0f));
|
||||||
|
if (f == 3) std::reverse(face.begin(), face.end()); // (turned in)
|
||||||
|
bad.faces.push_back(face);
|
||||||
|
if (f == 5) bad.faces.push_back(face); // (twice)
|
||||||
|
}
|
||||||
|
bad.faces.push_back({0, 1, 1}); // (no area)
|
||||||
|
const int a = int(bad.verts.size());
|
||||||
|
bad.verts.insert(bad.verts.end(), {{-0.5f, 0, -0.5f}, {0.5f, 0, -0.5f}, {0.5f, 0, 0.5f}, {-0.5f, 0, 0.5f}});
|
||||||
|
bad.faces.push_back({a, a + 1, a + 2, a + 3}), bad.faces.push_back({a + 3, a + 2, a + 1, a}); // (an inside wall, back to back)
|
||||||
|
CHECK(!checkForPrint(bad).closed(), "the broken cube is broken");
|
||||||
|
const Repair r = repairForPrint(bad);
|
||||||
|
const PrintCheck c = checkForPrint(bad);
|
||||||
|
CHECK(c.closed() && !c.insideOut && std::abs(c.volume - 1.0f) < 1e-4f && r.closedHoles == 1 && r.doubledFaces == 3 && r.emptyFaces == 1 && r.joinedPoints > 0,
|
||||||
|
"repair: closed, outwards, 1 m3 (joined %d, empty %d, doubled %d, turned %d, holes %d)", r.joinedPoints, r.emptyFaces, r.doubledFaces, r.turnedFaces, r.closedHoles);
|
||||||
|
Mesh fine = makeCube(1.0f);
|
||||||
|
CHECK(repairForPrint(fine).total() == 0 && fine.faces == good.faces, "a good cube: nothing to repair");
|
||||||
|
}
|
||||||
// Spin: a square beside the axis turned all the way round is a ring (Pappus: its area times
|
// Spin: a square beside the axis turned all the way round is a ring (Pappus: its area times
|
||||||
// how far its middle goes); a quarter turn has ends; a screw rises.
|
// how far its middle goes); a quarter turn has ends; a screw rises.
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue