Add CrossSection::EvenOdd for even-odd input fill (#1788)

This commit is contained in:
Zach Loafman
2026-08-10 21:10:21 +12:00
committed by GitHub
parent ff42ddc885
commit c50001b82d
21 changed files with 342 additions and 23 deletions
+10
View File
@@ -40,6 +40,16 @@ ManifoldCrossSection* manifold_cross_section_of_polygons(void* mem,
return to_c(new (mem) CrossSection(*from_c(p)));
}
ManifoldCrossSection* manifold_cross_section_even_odd_simple_polygon(
void* mem, ManifoldSimplePolygon* p) {
return to_c(new (mem) CrossSection(CrossSection::EvenOdd(*from_c(p))));
}
ManifoldCrossSection* manifold_cross_section_even_odd_polygons(
void* mem, ManifoldPolygons* p) {
return to_c(new (mem) CrossSection(CrossSection::EvenOdd(*from_c(p))));
}
ManifoldCrossSectionVec* manifold_cross_section_empty_vec(void* mem) {
return to_c(new (mem) CrossSectionVec());
}
+4
View File
@@ -303,6 +303,10 @@ ManifoldCrossSection* manifold_cross_section_of_simple_polygon(
void* mem, ManifoldSimplePolygon* p);
ManifoldCrossSection* manifold_cross_section_of_polygons(void* mem,
ManifoldPolygons* p);
ManifoldCrossSection* manifold_cross_section_even_odd_simple_polygon(
void* mem, ManifoldSimplePolygon* p);
ManifoldCrossSection* manifold_cross_section_even_odd_polygons(
void* mem, ManifoldPolygons* p);
ManifoldCrossSection* manifold_cross_section_square(void* mem, double x,
double y, int center);
ManifoldCrossSection* manifold_cross_section_circle(void* mem, double radius,
+1
View File
@@ -17,6 +17,7 @@ def all_cross_section():
poly = [[0, 0], [1, 0], [1, 1]]
c = CrossSection([np.array(poly)])
c = CrossSection([poly])
c = CrossSection.even_odd([poly])
c = CrossSection() + c
a = c.area()
c = CrossSection.batch_boolean(
+3
View File
@@ -881,6 +881,9 @@ NB_MODULE(manifold3d, m) {
.def(nb::init<>(), cross_section__cross_section)
.def(nb::init<std::vector<std::vector<vec2>>>(), nb::arg("contours"),
cross_section__cross_section__contours)
.def_static("even_odd",
nb::overload_cast<const Polygons&>(&CrossSection::EvenOdd),
nb::arg("contours"), cross_section__even_odd__contours)
.def("area", &CrossSection::Area, cross_section__area)
.def("num_vert", &CrossSection::NumVert, cross_section__num_vert)
.def("num_contour", &CrossSection::NumContour, cross_section__num_contour)
+1
View File
@@ -152,6 +152,7 @@ EMSCRIPTEN_BINDINGS(whatever) {
select_overload<CrossSection() const>(&CrossSection::Hull));
// CrossSection Static Methods
function("_EvenOdd", &cross_js::EvenOdd);
function("_Square", &CrossSection::Square);
function("_Circle", &CrossSection::Circle);
function("_crossSectionUnionN", &cross_js::UnionN);
+9 -2
View File
@@ -544,6 +544,13 @@ Module.setup = function() {
return new Module.CrossSection(polygons);
};
Module.CrossSection.evenOdd = function(polygons) {
const polygonsVec = polygons2vec(polygons);
const cs = Module._EvenOdd(polygonsVec);
disposePolygons(polygonsVec);
return cs;
};
Module.CrossSection.square = function(...args) {
let size = undefined;
if (args.length == 0)
@@ -675,7 +682,7 @@ Module.setup = function() {
scaleTop = [1.0, 1.0], center = false) {
const cs = (polygons instanceof CrossSectionCtor) ?
polygons :
Module.CrossSection(polygons, 'Positive');
Module.CrossSection(polygons);
return cs.extrude(height, nDivisions, twistDegrees, scaleTop, center);
};
@@ -683,7 +690,7 @@ Module.setup = function() {
polygons, circularSegments = 0, revolveDegrees = 360.0) {
const cs = (polygons instanceof CrossSectionCtor) ?
polygons :
Module.CrossSection(polygons, 'Positive');
Module.CrossSection(polygons);
return cs.revolve(circularSegments, revolveDegrees);
};
+4
View File
@@ -119,6 +119,10 @@ CrossSection OfPolygons(std::vector<std::vector<vec2>> polygons) {
return CrossSection(polygons);
}
CrossSection EvenOdd(std::vector<std::vector<vec2>> polygons) {
return CrossSection::EvenOdd(polygons);
}
CrossSection Union(const CrossSection& a, const CrossSection& b) {
return a + b;
}
+1 -1
View File
@@ -66,7 +66,7 @@ const manifoldMemberFunctions = [
// CrossSection static methods (that return a new cross-section)
const crossSectionStaticFunctions = [
'square', 'circle', 'union', 'difference', 'intersection', 'ofPolygons',
'hull'
'evenOdd', 'hull'
];
// CrossSection member functions (that return a new cross-section)
const crossSectionMemberFunctions = [
+20 -6
View File
@@ -102,9 +102,9 @@ export function resetToCircularDefaults(): void;
export class CrossSection {
/**
* Create a 2d cross-section from a set of contours (complex polygons). A
* boolean union operation (with Positive filling rule by default) is
* performed to combine overlapping polygons and ensure the resulting
* CrossSection is free of intersections.
* boolean union operation (with the Positive filling rule) is performed to
* combine overlapping polygons and ensure the resulting CrossSection is free
* of intersections.
*
* @param contours A set of closed paths describing zero or more complex
* polygons.
@@ -369,9 +369,9 @@ export class CrossSection {
/**
* Create a 2d cross-section from a set of contours (complex polygons). A
* boolean union operation (with Positive filling rule by default) is
* performed to combine overlapping polygons and ensure the resulting
* CrossSection is free of intersections.
* boolean union operation (with the Positive filling rule) is performed to
* combine overlapping polygons and ensure the resulting CrossSection is free
* of intersections.
*
* @param contours A set of closed paths describing zero or more complex
* polygons.
@@ -379,6 +379,20 @@ export class CrossSection {
*/
static ofPolygons(contours: Polygons): CrossSection;
/**
* Create a 2d cross-section from a set of contours, reading them by the
* even-odd rule: a sub-region is filled when its winding number is odd, so
* overlapping an odd number of contours fills and overlapping an even number
* leaves a hole. Use this to take input authored for an even-odd tool such
* as OpenSCAD. The result is regularized like any other CrossSection, so
* every later operation behaves the same.
*
* @param contours A set of closed paths describing zero or more complex
* polygons.
* @group Input & Output
*/
static evenOdd(contours: Polygons): CrossSection;
/**
* Return the contours of this CrossSection as a list of simple polygons.
* @group Input & Output
+15 -1
View File
@@ -1,6 +1,6 @@
import {beforeAll, expect, suite, test} from 'vitest';
import Module, {type ManifoldToplevel} from '../manifold'
import Module, {type ManifoldToplevel, type Polygons} from '../manifold'
let manifoldModule: ManifoldToplevel;
@@ -26,6 +26,20 @@ suite('CrossSection Bindings', () => {
expect(cs.area()).to.be.greaterThan(0);
});
test('evenOdd fills by parity, not sign', () => {
const nested: Polygons = [
[[-2, -2], [2, -2], [2, 2], [-2, 2]],
[[-1, -1], [1, -1], [1, 1], [-1, 1]],
];
// Positive fill leaves the nested pair solid; even-odd cuts the inner
// square out.
expect(manifoldModule.CrossSection.ofPolygons(nested).area())
.toBeCloseTo(16);
const evenOdd = manifoldModule.CrossSection.evenOdd(nested);
expect(evenOdd.numContour()).toEqual(2);
expect(evenOdd.area()).toBeCloseTo(12);
});
test('simplify argument is defaulted', () => {
const cs = manifoldModule.CrossSection.circle(1).simplify();
expect(cs.numContour()).toEqual(1);
+8 -1
View File
@@ -123,8 +123,15 @@ Boolean2 predicates are:
multiplicity, then using `Add`.
- `Intersect`: `w > 1`, which corresponds to both operands covering a side for
normalized unit-winding operands.
- `EvenOdd`: `w` odd, reachable from `CrossSection::EvenOdd` for input authored
against an even-odd tool. It reads the same signed winding by parity rather
than sign, so unlike `Add` it fills a lone clockwise contour (`w == -1`).
Boolean2 construction is Positive-only.
The rule is not selectable for boolean operations - `Boolean2D` picks `Add` or
`Intersect` from the `OpType`. It is selectable only where the input's own
winding is first read, which is single-operand construction. What gets stored is
normal-oriented and positive either way, so nothing downstream sees the
difference.
## Regularization And Epsilon
+2
View File
@@ -66,6 +66,8 @@ class CrossSection {
CrossSection(const SimplePolygon& contour);
CrossSection(const Polygons& contours);
CrossSection(const Rect& rect);
static CrossSection EvenOdd(const SimplePolygon& contour);
static CrossSection EvenOdd(const Polygons& contours);
Polygons ToPolygons() const;
///@}
+6 -6
View File
@@ -215,12 +215,12 @@ Polygons OutEdgesToPolygons(const std::vector<vec2>& verts,
return polys;
}
// Apply the Positive (Add) winding rule to one polygon set, regularizing it at
// machine-scale eps with no extra tolerance. This is fill-rule application -
// what construction and Offset use to resolve self-intersections - as opposed
// to CrossSection::Simplify, which decimates at a user-designated tolerance.
Polygons ApplyFillRule(const Polygons& polys, double eps) {
return ApplyFillRule(polys, {}, 1, WindRule::Add, eps);
// Apply a winding rule to one polygon set, regularizing it at machine-scale eps
// with no extra tolerance. This is fill-rule application - what construction
// and Offset use to resolve self-intersections - as opposed to
// CrossSection::Simplify, which decimates at a user-designated tolerance.
Polygons ApplyFillRule(const Polygons& polys, double eps, WindRule rule) {
return ApplyFillRule(polys, {}, 1, rule, eps);
}
// Infer eps from a polygon set's absolute coordinate scale via Smith's
+8 -3
View File
@@ -168,6 +168,7 @@ struct Trace;
enum class WindRule {
Add,
Intersect,
EvenOdd,
};
// Smith sweep-line arrangement + winding over the collapsed input edges: a
@@ -202,9 +203,13 @@ std::pair<std::vector<vec2>, std::vector<EdgeM>> PolygonsToInput(
Polygons OutEdgesToPolygons(const std::vector<vec2>& verts,
const std::vector<OutEdge>& edges);
// Regularize one polygon set under the Positive (Add) winding rule at
// machine-scale eps. Fill-rule application, not tolerance decimation.
Polygons ApplyFillRule(const Polygons& polys, double eps);
// Regularize one polygon set at machine-scale eps under `rule`, which decides
// how the input's own winding is read: Positive (Add) or EvenOdd. Either way
// the output is normal-oriented and positive, so this is the only place the
// rule is visible. No default: every caller states which rule it wants, since
// only CrossSection construction has a reason to pick anything but Add.
// Fill-rule application, not tolerance decimation.
Polygons ApplyFillRule(const Polygons& polys, double eps, WindRule rule);
Polygons Boolean2D(const Polygons& a, const Polygons& b, OpType op,
double eps = 0.0);
+2
View File
@@ -34,6 +34,8 @@ std::string WindRuleName(WindRule rule) {
return "Add";
case WindRule::Intersect:
return "Intersect";
case WindRule::EvenOdd:
return "EvenOdd";
}
return "Unknown";
}
+1 -1
View File
@@ -281,7 +281,7 @@ Polygons Offset(const Polygons& in, double delta, JoinType jt,
// so Positive/Add cleanup keeps the filled side for both dilation and inset.
// This is fill-rule application at machine eps, not tolerance decimation -
// the caller can Simplify the result if it wants collinear verts removed.
return ApplyFillRule(offsetRings, eps);
return ApplyFillRule(offsetRings, eps, WindRule::Add);
}
// ===== Containment decompose =====
+4
View File
@@ -79,12 +79,16 @@ enum class Side : uint8_t { UNDER, OVER, ON, ENDS };
// Positive fill (Add: w > 0), intersection (Intersect: w > 1). Subtract feeds
// the second operand with negative multiplicity, so it uses the Add rule.
// EvenOdd reads the same signed winding by parity, so unlike Add it fills a
// lone clockwise contour (w == -1) as well as a counter-clockwise one.
bool IsInside(WindRule rule, int64_t w) {
switch (rule) {
case WindRule::Add:
return w > 0;
case WindRule::Intersect:
return w > 1;
case WindRule::EvenOdd:
return w % 2 != 0;
}
return false;
}
+42 -2
View File
@@ -263,7 +263,45 @@ CrossSection::CrossSection(const SimplePolygon& contour)
*/
CrossSection::CrossSection(const Polygons& contours) {
tolerance_ = InferEps(contours, {});
paths_ = shared_paths(ApplyFillRule(contours, tolerance_));
paths_ = shared_paths(ApplyFillRule(contours, tolerance_, WindRule::Add));
}
/**
* Create a 2d cross-section from a single contour, reading it by the even-odd
* rule rather than the positive rule the constructors use. See
* CrossSection::EvenOdd(const Polygons&).
*
* @param contour A closed path outlining the desired cross-section.
*/
CrossSection CrossSection::EvenOdd(const SimplePolygon& contour) {
return EvenOdd(Polygons{contour});
}
/**
* Create a 2d cross-section from a set of contours, reading them by the
* even-odd rule: a sub-region is filled when its winding number is odd, so
* overlapping an odd number of contours fills and overlapping an even number
* leaves a hole. The constructors instead use the positive rule (filled where
* the winding number is greater than zero), which ignores contour count and
* only cares about direction. Use this to take input authored for an even-odd
* tool such as OpenSCAD.
*
* The rule only decides how these contours are read. The result is regularized
* and normal-oriented like any other CrossSection, so every later operation
* behaves the same. Note that Manifold::Extrude and Manifold::Revolve take raw
* contours straight to the triangulator without applying any fill rule, so
* even-odd input has to be regularized here first, by passing
* EvenOdd(contours).ToPolygons() to them.
*
* @param contours A set of closed paths describing zero or more complex
* polygons.
*/
CrossSection CrossSection::EvenOdd(const Polygons& contours) {
const double eps = InferEps(contours, {});
CrossSection out(
shared_paths(ApplyFillRule(contours, eps, WindRule::EvenOdd)));
out.tolerance_ = eps;
return out;
}
/**
@@ -569,7 +607,9 @@ CrossSection CrossSection::WarpBatch(
// Warping can self-intersect the rings, so re-apply the fill rule at machine
// eps (not the drift tolerance - this is regularization, not decimation).
const double eps = InferEps(paths, {});
CrossSection out(shared_paths(ApplyFillRule(paths, eps)));
// Positive regardless of how this section was originally read: a CrossSection
// carries no fill rule, and its stored geometry is already regularized.
CrossSection out(shared_paths(ApplyFillRule(paths, eps, WindRule::Add)));
out.tolerance_ = std::max(tolerance_, eps);
return out;
}
+21
View File
@@ -577,6 +577,27 @@ TEST(Boolean2, CleanupPassMatchesValidAddSinglePass) {
EXPECT_NEAR(TotalSignedArea(pass1Polys), TotalSignedArea(pass2Polys), 1e-12);
}
// EvenOdd only changes which sub-edges the winding pass retains: `rule` is
// read in SweepMode::Winding and nowhere in the arrangement. So on the same
// self-intersecting input it must produce a graph the independent oracle still
// accepts, over the same vertex merge Add gets.
TEST(Boolean2, EvenOddRetainedGraphMatchesAddArrangement) {
Polygons polys{RandomTopologicalRing(8, 15)};
const double eps = InferEps(polys, {});
const auto [verts, edges] = PolygonsToInput(polys);
const auto add =
RemoveOverlaps2D(verts, edges, eps, /*debug=*/false, WindRule::Add);
const auto evenOdd =
RemoveOverlaps2D(verts, edges, eps, /*debug=*/false, WindRule::EvenOdd);
EXPECT_TRUE(CheckRetainedGraphValidity(
evenOdd, edges, evenOdd.inputVert2Merged, evenOdd.numMergedVerts, eps));
EXPECT_EQ(evenOdd.numMergedVerts, add.numMergedVerts);
EXPECT_EQ(evenOdd.inputVert2Merged, add.inputVert2Merged);
EXPECT_FALSE(OutEdgesToPolygons(evenOdd.verts, evenOdd.edges).empty());
}
TEST(Boolean2, OffsetRoundUsesRequestedSegments) {
SimplePolygon square = {{0, 0}, {20, 0}, {20, 20}, {0, 20}};
const int segments = 20;
+130
View File
@@ -176,6 +176,136 @@ TEST(CrossSection, Square) {
EXPECT_FLOAT_EQ((a - b).Volume(), 0.);
}
// Two same-direction squares, one nested in the other. Positive fill only
// cares that the winding is above zero, so the pair is solid; even-odd sees
// winding 2 in the middle and cuts a hole.
TEST(CrossSection, EvenOddNestedContours) {
const Polygons nested = {
{{-2, -2}, {2, -2}, {2, 2}, {-2, 2}},
{{-1, -1}, {1, -1}, {1, 1}, {-1, 1}},
};
const CrossSection positive(nested);
EXPECT_EQ(positive.NumContour(), 1u);
EXPECT_NEAR(positive.Area(), 16., 1e-9);
const CrossSection evenOdd = CrossSection::EvenOdd(nested);
EXPECT_EQ(evenOdd.NumContour(), 2u);
EXPECT_NEAR(evenOdd.Area(), 12., 1e-9);
}
// Reverse the inner square and the two rules agree: winding 0 is neither
// positive nor odd, so the normal hole convention is unaffected.
TEST(CrossSection, EvenOddReversedInnerIsAHoleEitherWay) {
const Polygons ring = {
{{-2, -2}, {2, -2}, {2, 2}, {-2, 2}},
{{-1, -1}, {-1, 1}, {1, 1}, {1, -1}},
};
const CrossSection positive(ring);
const CrossSection evenOdd = CrossSection::EvenOdd(ring);
EXPECT_NEAR(positive.Area(), 12., 1e-9);
EXPECT_NEAR(evenOdd.Area(), 12., 1e-9);
EXPECT_EQ(positive.NumContour(), evenOdd.NumContour());
}
// One contour crossing itself into two lobes of opposite winding. Positive
// fill keeps only the counter-clockwise lobe; even-odd keeps both, since +1
// and -1 are both odd.
TEST(CrossSection, EvenOddBowtieKeepsBothLobes) {
const SimplePolygon bowtie = {{0, 0}, {2, 2}, {0, 2}, {2, 0}};
const CrossSection positive(bowtie);
EXPECT_EQ(positive.NumContour(), 1u);
EXPECT_NEAR(positive.Area(), 1., 1e-9);
const CrossSection evenOdd = CrossSection::EvenOdd(bowtie);
EXPECT_EQ(evenOdd.NumContour(), 2u);
EXPECT_NEAR(evenOdd.Area(), 2., 1e-9);
}
// Three same-direction nested squares alternate filled/hole/filled by depth.
// Area() is the signed total, so 36 - 16 + 4 also pins the stored contours to
// alternating orientation - a counter-clockwise middle ring would give 56.
TEST(CrossSection, EvenOddNestingAlternatesByDepth) {
const Polygons nested = {
{{-3, -3}, {3, -3}, {3, 3}, {-3, 3}},
{{-2, -2}, {2, -2}, {2, 2}, {-2, 2}},
{{-1, -1}, {1, -1}, {1, 1}, {-1, 1}},
};
const CrossSection positive(nested);
EXPECT_EQ(positive.NumContour(), 1u);
EXPECT_NEAR(positive.Area(), 36., 1e-9);
const CrossSection evenOdd = CrossSection::EvenOdd(nested);
EXPECT_EQ(evenOdd.NumContour(), 3u);
EXPECT_NEAR(evenOdd.Area(), 24., 1e-9);
// Probe each depth so the totals can't hide a hole in the wrong place.
const CrossSection probe = CrossSection::Square({0.5, 0.5}, true);
EXPECT_NEAR((evenOdd ^ probe).Area(), 0.25, 1e-9);
EXPECT_TRUE((evenOdd ^ probe.Translate({1.5, 0})).IsEmpty());
EXPECT_NEAR((evenOdd ^ probe.Translate({2.5, 0})).Area(), 0.25, 1e-9);
}
// Even-odd goes by parity, not sign, so a lone clockwise contour (winding -1)
// is filled where positive fill discards it.
TEST(CrossSection, EvenOddFillsAClockwiseContour) {
const SimplePolygon clockwise = {{0, 0}, {0, 2}, {2, 2}, {2, 0}};
EXPECT_TRUE(CrossSection(clockwise).IsEmpty());
const CrossSection evenOdd = CrossSection::EvenOdd(clockwise);
EXPECT_EQ(evenOdd.NumContour(), 1u);
EXPECT_NEAR(evenOdd.Area(), 4., 1e-9);
}
// The single-contour overload. A pentagram's core is wound twice by one
// self-crossing path, so positive fills the whole silhouette and even-odd
// leaves the core hollow.
TEST(CrossSection, EvenOddSelfIntersectingContour) {
SimplePolygon pentagram;
for (int i = 0; i < 5; ++i) {
const double theta = kPi / 2 + i * 4 * kPi / 5;
pentagram.push_back({std::cos(theta), std::sin(theta)});
}
// Points sit on the unit circle and the self-crossings on a circle of
// radius cos(72 deg) / cos(36 deg). The silhouette is the 10-gon through
// both, and the twice-wound core is the inner regular pentagon.
const double r = std::cos(2 * kPi / 5) / std::cos(kPi / 5);
const double silhouette = 5 * r * std::sin(kPi / 5);
const double core = 2.5 * r * r * std::sin(2 * kPi / 5);
const CrossSection positive(pentagram);
EXPECT_EQ(positive.NumContour(), 1u);
EXPECT_NEAR(positive.Area(), silhouette, 1e-9);
const CrossSection evenOdd = CrossSection::EvenOdd(pentagram);
EXPECT_EQ(evenOdd.NumContour(), 2u);
EXPECT_NEAR(evenOdd.Area(), silhouette - core, 1e-9);
}
// The rule only decides how the input contours are read. What gets stored is
// normal-oriented and positive either way, so re-reading the output with the
// default rule changes nothing and later ops see an ordinary CrossSection.
TEST(CrossSection, EvenOddOutputIsCanonicalPositive) {
const Polygons nested = {
{{-2, -2}, {2, -2}, {2, 2}, {-2, 2}},
{{-1, -1}, {1, -1}, {1, 1}, {-1, 1}},
};
const CrossSection evenOdd = CrossSection::EvenOdd(nested);
const CrossSection reread(evenOdd.ToPolygons());
EXPECT_EQ(reread.NumContour(), evenOdd.NumContour());
EXPECT_NEAR(reread.Area(), evenOdd.Area(), 1e-9);
// The hole survives a boolean against a shape covering the whole thing.
const CrossSection box(CrossSection::Square({5, 5}, true));
EXPECT_NEAR((evenOdd ^ box).Area(), evenOdd.Area(), 1e-9);
EXPECT_NEAR((box - evenOdd).Area(), 13., 1e-9);
}
TEST(CrossSection, MirrorUnion) {
auto a = CrossSection::Square({5., 5.}, true);
auto b = a.Translate({2.5, 2.5});
+50
View File
@@ -760,6 +760,56 @@ TEST(CBIND, run_flag_accessors) {
free(cube);
}
TEST(CBIND, cross_section_even_odd) {
// Two same-direction squares, one nested in the other: positive fill leaves
// the pair solid, even-odd cuts the inner square out.
ManifoldVec2 outerPts[] = {{-2, -2}, {2, -2}, {2, 2}, {-2, 2}};
ManifoldVec2 innerPts[] = {{-1, -1}, {1, -1}, {1, 1}, {-1, 1}};
ManifoldSimplePolygon* outer =
manifold_simple_polygon(alloc_simple_polygon_buffer(), outerPts, 4);
ManifoldSimplePolygon* inner =
manifold_simple_polygon(alloc_simple_polygon_buffer(), innerPts, 4);
ManifoldSimplePolygon* sps[] = {outer, inner};
ManifoldPolygons* ps = manifold_polygons(alloc_polygons_buffer(), sps, 2);
ManifoldCrossSection* positive = manifold_cross_section_of_polygons(
malloc(manifold_cross_section_size()), ps);
ManifoldCrossSection* evenOdd = manifold_cross_section_even_odd_polygons(
malloc(manifold_cross_section_size()), ps);
EXPECT_NEAR(manifold_cross_section_area(positive), 16.0, 1e-9);
EXPECT_NEAR(manifold_cross_section_area(evenOdd), 12.0, 1e-9);
// The single-contour entry point, on a clockwise square: even-odd goes by
// parity so it fills, while positive fill discards the negative winding.
ManifoldVec2 cwPts[] = {{0, 0}, {0, 2}, {2, 2}, {2, 0}};
ManifoldSimplePolygon* cw =
manifold_simple_polygon(alloc_simple_polygon_buffer(), cwPts, 4);
ManifoldCrossSection* cwPositive = manifold_cross_section_of_simple_polygon(
malloc(manifold_cross_section_size()), cw);
ManifoldCrossSection* cwEvenOdd =
manifold_cross_section_even_odd_simple_polygon(
malloc(manifold_cross_section_size()), cw);
EXPECT_NEAR(manifold_cross_section_area(cwPositive), 0.0, 1e-9);
EXPECT_NEAR(manifold_cross_section_area(cwEvenOdd), 4.0, 1e-9);
manifold_destruct_simple_polygon(outer);
manifold_destruct_simple_polygon(inner);
manifold_destruct_simple_polygon(cw);
manifold_destruct_polygons(ps);
manifold_destruct_cross_section(positive);
manifold_destruct_cross_section(evenOdd);
manifold_destruct_cross_section(cwPositive);
manifold_destruct_cross_section(cwEvenOdd);
free(outer);
free(inner);
free(cw);
free(ps);
free(positive);
free(evenOdd);
free(cwPositive);
free(cwEvenOdd);
}
TEST(CBIND, cross_section_tolerance) {
void* buf = malloc(manifold_cross_section_size());