From 14e35cceb16f0cd5362d98f7d7725d239d143833 Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Wed, 21 Oct 2026 07:51:07 +0200 Subject: [PATCH] Print: walls thinner than the least (0.8 mm, set it) shown in yellow, the thinnest told; measured once per shape and size, not each move; tests Co-Authored-By: Claude Opus 5.5 --- src/app.cpp | 56 +++++++++++++++++++++++++++++++++++++++++++-- src/bvh.cpp | 17 ++++++++++++++ src/bvh.h | 4 ++++ tests/mesh_test.cpp | 9 ++++++++ 4 files changed, 84 insertions(+), 2 deletions(-) diff --git a/src/app.cpp b/src/app.cpp index 45113fc..3d268b2 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -249,6 +249,13 @@ struct PrintInfo { glm::vec3 lo{0.0f}, hi{0.0f}; keel::ModelId overhang{}; bool hasOverhang = false; + // Walls too thin to print: how thick at each face (worked out again only when its shape or + // size changes: moving or turning it doesn't), how many under the least, the thinnest. + uint64_t thickKey = ~0ull; + std::vector thick; + int thinFaces = 0; + float thinnest = 1e30f; + keel::ModelId thin{}; }; keel::Cvar fry_printer("fry_printer", "0", keel::CVAR_ARCHIVE, "Fry3DX: the 3D printer (a number in the Print tab's list)"); @@ -931,7 +938,8 @@ private: // The Print tab. PrintTool printTool_ = PrintTool::None; std::vector measure_; - bool measureSnap_ = true, showOverhangs_ = true, sizing_ = false; + bool measureSnap_ = true, showOverhangs_ = true, sizing_ = false, showThin_ = true; + float minWall_ = 0.0008f; // (m: the thinnest wall that prints well, twice a 0.4 mm nozzle) float overhangDeg_ = 45.0f, infill_ = 15.0f; int material_ = 0; std::map printInfos_; @@ -3007,7 +3015,7 @@ private: } const PrintInfo& printInfo(const Object& o) { PrintInfo& p = printInfos_[o.id]; - uint64_t key = versions_[o.id] * 1000003ull + uint64_t(overhangDeg_ * 10); + uint64_t key = versions_[o.id] * 1000003ull + uint64_t(overhangDeg_ * 10) + uint64_t(minWall_ * 1e5f) * 7919ull; for (float f : {o.position.x, o.position.y, o.position.z, o.rotation.x, o.rotation.y, o.rotation.z, o.rotation.w, o.scale.x, o.scale.y, o.scale.z}) { uint32_t b; std::memcpy(&b, &f, 4); @@ -3039,6 +3047,33 @@ private: km.submeshes.push_back({0, uint32_t(km.indices.size()), 0}); p.overhang = r_->updateModel(std::format("fry:overhang:{}", o.id), km); } + // Thin walls (only of a closed shape: an open one has no inside to be thick). + uint64_t thickKey = versions_[o.id] * 1000003ull; + for (float f : {o.scale.x, o.scale.y, o.scale.z}) { + uint32_t b; + std::memcpy(&b, &f, 4); + thickKey = (thickKey ^ b) * 1099511628211ull; + } + if (p.thickKey != thickKey || p.thick.size() != p.world.faces.size()) + p.thickKey = thickKey, p.thick = p.check.closed() ? fry::wallThickness(p.world) : std::vector(p.world.faces.size(), 1e30f); + p.thinFaces = 0, p.thinnest = 1e30f; + km.vertices.clear(), km.indices.clear(), km.submeshes.clear(); + for (size_t f = 0; f < p.world.faces.size(); ++f) { + p.thinnest = std::min(p.thinnest, p.thick[f]); + if (p.thick[f] >= minWall_) continue; + ++p.thinFaces; + const glm::vec3 n = faceNormal(p.world, int(f)); + for (const auto& t : triangulate(p.world, int(f))) + for (int k : t) { + const glm::vec3 q = p.world.verts[size_t(k)] + n * 0.0005f; + km.indices.push_back(uint32_t(km.vertices.size())); + km.vertices.push_back({{q.x, q.y, q.z}, {n.x, n.y, n.z}, {0, 0}, {0, 0}}); + } + } + if (p.thinFaces) { + km.submeshes.push_back({0, uint32_t(km.indices.size()), 0}); + p.thin = r_->updateModel(std::format("fry:thin:{}", o.id), km); + } return p; } std::vector printTargets() { // (what's picked, or everything shown) @@ -3082,6 +3117,13 @@ private: if (const PrintInfo& p = printInfo(o); p.hasOverhang) r.drawModel(p.overhang, glm::scale(glm::mat4(1.0f), glm::vec3(kUnits)), glm::vec4(0.95f, 0.15f, 0.1f, 0.55f), nullptr, &flat, false); } + if (showThin_) { + const keel::ModelLighting flat{{glm::vec3(1.0f), glm::vec3(1.0f), glm::vec3(1.0f), glm::vec3(1.0f), glm::vec3(1.0f), glm::vec3(1.0f)}, 0.0f}; + for (const Object& o : scene_.objects) + if (o.visible) + if (const PrintInfo& p = printInfo(o); p.thinFaces) + r.drawModel(p.thin, glm::scale(glm::mat4(1.0f), glm::vec3(kUnits)), glm::vec4(1.0f, 0.8f, 0.1f, 0.7f), nullptr, &flat, false); + } } void putOnBed() { const auto targets = printTargets(); @@ -3380,6 +3422,16 @@ private: ImGui::SetNextItemWidth(w); ImGui::SliderFloat("##hang", &overhangDeg_, 20.0f, 80.0f, "steeper than %.0f degrees"); if (ImGui::IsItemHovered()) ImGui::SetTooltip("How far from upright a face can lean over before it needs support (45 for most printers)"); + int thin = 0; + float thinnest = 1e30f; + for (Object* o : targets) thin += printInfo(*o).thinFaces, thinnest = std::min(thinnest, printInfo(*o).thinnest); + if (thin) bad(std::format("{} face{} on walls thinner than {:.1f} mm (yellow), the thinnest {:.2f} mm: it may not print there, or break.", thin, thin == 1 ? "" : "s", minWall_ * 1000, thinnest * 1000)); + else if (all.openEdges == 0) good(std::format("Walls {:.1f} mm thick or more", minWall_ * 1000).c_str()); + ImGui::Checkbox("Show thin walls in yellow", &showThin_); + float mm = minWall_ * 1000; + ImGui::SetNextItemWidth(w); + if (ImGui::SliderFloat("##minwall", &mm, 0.2f, 5.0f, "thinner than %.1f mm")) minWall_ = mm / 1000; + if (ImGui::IsItemHovered()) ImGui::SetTooltip("The thinnest wall your printer makes well: about twice the nozzle (0.8 mm for a 0.4 mm nozzle)"); ImGui::SeparatorText("Material"); ImGui::SetNextItemWidth(w); diff --git a/src/bvh.cpp b/src/bvh.cpp index bd8035a..3997539 100644 --- a/src/bvh.cpp +++ b/src/bvh.cpp @@ -116,4 +116,21 @@ bool TriangleTree::blocked(const glm::vec3& o, const glm::vec3& d, float maxT, i return false; } +std::vector wallThickness(const Mesh& m) { + const TriangleTree tree(m, glm::mat4(1.0f)); + std::vector out(m.faces.size(), 1e30f); + for (size_t f = 0; f < m.faces.size(); ++f) { + const auto& fv = m.faces[f]; + if (fv.size() < 3) continue; + glm::vec3 c(0.0f); + for (int v : fv) c += m.verts[size_t(v)]; + c /= float(fv.size()); + const glm::vec3 n = faceNormal(m, int(f)); + // (a hair inside, so the face itself isn't what it meets) + const glm::vec3 from = c - n * 1e-6f; + if (auto h = tree.nearest(from, -n)) out[f] = h->t + 1e-6f; + } + return out; +} + } // namespace fry diff --git a/src/bvh.h b/src/bvh.h index 9a54836..664a6d8 100644 --- a/src/bvh.h +++ b/src/bvh.h @@ -45,6 +45,10 @@ private: int build(int first, int count); }; +// How thick the shape is at each face: from its middle straight in (against its normal) to the +// surface on the other side (1e30 where nothing's there: an open shape, a face turned in). +std::vector wallThickness(const Mesh& m); + // Where a ray meets a triangle (Möller–Trumbore), in the ray's lengths. std::optional rayTriangle(const glm::vec3& origin, const glm::vec3& dir, const glm::vec3& a, const glm::vec3& b, const glm::vec3& c); diff --git a/tests/mesh_test.cpp b/tests/mesh_test.cpp index 174bec7..b4eb303 100644 --- a/tests/mesh_test.cpp +++ b/tests/mesh_test.cpp @@ -449,6 +449,15 @@ int main() { CHECK(checkForPrint(o).closed() && checkForPrint(o).volume < 0.05f * 0.04f * 0.01f, "an O has its hole (%g m3)", checkForPrint(o).volume); CHECK(makeText({}, "x", st, &err).faces.empty() && !err.empty(), "no font: nothing, and why"); } + // Wall thickness: a box 2 mm thin one way is 2 mm thick on its big faces, 1 m the other ways. + { + Mesh slab = makeCube(1.0f); + transform(slab, glm::scale(glm::mat4(1), glm::vec3(1.0f, 1.0f, 0.002f))); + const std::vector t = wallThickness(slab); + int thin = 0, wide = 0; + for (size_t f = 0; f < t.size(); ++f) thin += std::abs(t[f] - 0.002f) < 1e-5f, wide += std::abs(t[f] - 1.0f) < 1e-4f; + CHECK(thin == 2 && wide == 4, "wall thickness: 2 faces 2 mm, 4 faces 1 m (%d, %d)", thin, wide); + } // The tree of boxes finds the same nearest hit as trying every triangle (and the same "in the way"). { Mesh m = makeSphere(32, 16, 1.0f);