Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"

[project]
name = "openscad_cpp_evaluator"
version = "0.13.9"
version = "0.13.10"
description = "C++ OpenSCAD evaluator with Python bindings"
readme = "README.md"
requires-python = ">=3.12"
Expand Down
18 changes: 17 additions & 1 deletion src/builtins/booleans.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,23 @@ std::vector<ColoredBody> generateCsg(Evaluator& ev, const CSGParams& params, con

std::vector<ColoredBody> bodies3d, sections2d;
for (const ColoredBody& c : split.foreground) {
if (c.body) bodies3d.push_back(c);
// A body whose own Manifold::Status() isn't NoError (e.g.
// NonFiniteVertex, from a degenerate accumulated transform deep
// in an unrelated ancestor's positioning math -- found via a
// real user script, snappy-reprap's z_tower_assembly chain,
// where one already-invalid sub-part silently zeroed out an
// entire 65-operand union of otherwise-valid geometry) must
// never reach Manifold's own `+`/`-`/`^` operators: unlike a
// genuinely empty operand (0 triangles, NoError), Manifold
// propagates a non-NoError status through boolean ops onto the
// WHOLE result instead of treating it as a no-op contributor,
// so a single bad part silently discards every valid sibling.
// Real OpenSCAD's CGAL backend doesn't hit this at all here
// (more robust to the same input) -- dropping the one invalid
// operand and unioning everything else is the closest match to
// its own behavior available without new numerical-robustness
// work on Manifold's own boolean ops.
if (c.body && c.body->Status() == manifold::Manifold::Error::NoError) bodies3d.push_back(c);
}
for (const ColoredBody& c : split.foreground) {
if (c.section) sections2d.push_back(c);
Expand Down
19 changes: 19 additions & 0 deletions tests/test_booleans.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,25 @@ TEST(Difference, EmptySubtractorLeavesBaseUnchanged) {
EXPECT_NEAR(e.bodies[0].body->Volume(), 64.0, 1e-6);
}

TEST(Union, OneInvalidOperandDoesNotDiscardValidSiblings) {
// Regression: Manifold's own `+` propagates a non-NoError Status()
// (e.g. NonFiniteVertex, from a degenerate NaN-vertex polyhedron here --
// in the wild, from an accumulated transform deep in an unrelated
// ancestor's own positioning math) onto the WHOLE combined result
// instead of treating the bad operand as a no-op contributor. Before
// filtering invalid operands out in generateCsg, a single bad part
// anywhere in a many-operand union() silently discarded every valid
// sibling too -- found via a real user project (snappy-reprap) where
// this zeroed out an entire ~65-part extruder assembly with no warning.
Evaluated e = evalSrc("union() {"
" polyhedron(points=[[0,0,0],[2,0,0],[0,2,0],[0/0,0,2]],"
" faces=[[0,1,2],[0,3,1],[0,2,3],[1,3,2]]);"
" translate([10,0,0]) cube(2);"
"}");
ASSERT_EQ(e.bodies.size(), 1u);
EXPECT_NEAR(e.bodies[0].body->Volume(), 8.0, 1e-6); // just the valid cube -- the NaN polyhedron is dropped, not fatal
}

// -- 2D-native boolean CSG (union/difference/intersection applied directly
// to 2D children, not mediated through linear_extrude()) -----------------

Expand Down