Add CrossSection::EvenOdd for even-odd input fill (#1788)
This commit is contained in:
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ std::string WindRuleName(WindRule rule) {
|
||||
return "Add";
|
||||
case WindRule::Intersect:
|
||||
return "Intersect";
|
||||
case WindRule::EvenOdd:
|
||||
return "EvenOdd";
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
@@ -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 =====
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user