Fuzz test (tests/fuzz_test.cpp): random shapes through random edits and combines, broken scene and model files; in check and CI. Fixed what it found: Manifold given repeated or non-finite points (crash; the BSP takes those now), a damaged file's non-finite numbers (now 0), extruding both sides of a plane (NaN)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Nexoniarz 2026-10-21 08:38:39 +02:00
commit 9918d9a770
13 changed files with 294 additions and 6 deletions

View file

@ -343,11 +343,18 @@ std::optional<Mesh> combineManifold(const Mesh& a, const Mesh& b, Combine op) {
manifold::MeshGL g;
g.numProp = 3;
g.vertProperties.reserve(m.verts.size() * 3);
for (const glm::vec3& v : m.verts) g.vertProperties.insert(g.vertProperties.end(), {v.x, v.y, v.z});
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) {
if (m.faces[f].size() < 3) return std::nullopt; // (a line: not a solid)
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)))
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 (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)

View file

@ -289,13 +289,14 @@ std::vector<int> extrudeFaces(Mesh& m, const std::set<int>& faces, float distanc
}
}
// One flat piece moves as one, along its normal; a bent one, each point along its own.
region = glm::length(region) > 1e-12f ? glm::normalize(region) : glm::vec3(0, 0, 1);
// (faces facing opposite ways, a plane's two sides: theirs cancel out; the first one's way)
region = glm::length(region) > 1e-12f ? glm::normalize(region) : faces.empty() ? glm::vec3(0, 0, 1) : faceNormal(m, *faces.begin());
bool flat = true;
for (int f : faces) flat &= glm::dot(faceNormal(m, f), region) > 0.99f;
std::map<int, int> moved;
std::vector<int> made;
for (auto& [v, n] : along) {
const glm::vec3 dir = flat ? region : glm::normalize(n);
const glm::vec3 dir = flat || glm::length(n) < 1e-9f ? region : glm::normalize(n);
moved[v] = int(m.verts.size());
made.push_back(moved[v]);
m.verts.push_back(m.verts[size_t(v)] + dir * distance);

View file

@ -423,6 +423,18 @@ std::optional<Scene> parseScene(std::istream& in, const std::string& path, std::
p = std::find_if(s.folders.begin(), s.folders.end(), [&](const Folder& g) { return g.id == p; })->parent;
}
for (Object& o : s.objects) o.folder = idAt(o.folder);
// A number that isn't one (a file damaged, or edited by hand: "1e999", "nan"): nothing there.
auto finite = [](glm::vec3& v, glm::vec3 instead) {
if (!std::isfinite(v.x) || !std::isfinite(v.y) || !std::isfinite(v.z)) v = instead;
};
for (Object& o : s.objects) {
for (glm::vec3& v : o.mesh.verts) finite(v, glm::vec3(0.0f));
finite(o.position, glm::vec3(0.0f)), finite(o.scale, glm::vec3(1.0f));
if (!std::isfinite(o.rotation.w) || !std::isfinite(o.rotation.x) || !std::isfinite(o.rotation.y) || !std::isfinite(o.rotation.z) || glm::length(o.rotation) < 1e-6f)
o.rotation = glm::quat(1, 0, 0, 0);
else
o.rotation = glm::normalize(o.rotation);
}
for (Object& o : s.objects) o.cuts = o.cuts >= 1 && o.cuts <= s.objects.size() && &s.objects[o.cuts - 1] != &o ? s.objects[o.cuts - 1].id : 0u;
return s;
}