|
|
|
@ -20,25 +20,32 @@ struct TestCase { |
|
|
|
// -----------------------------
|
|
|
|
// Helper: Print Point
|
|
|
|
// -----------------------------
|
|
|
|
std::ostream& operator<<(std::ostream& os, const Point2d& p) { |
|
|
|
return os << "(" << p.x() << ", " << p.y() << ")"; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
std::ostream& operator<<(std::ostream& os, const Point2d& p) { return os << "(" << p.x() << ", " << p.y() << ")"; } |
|
|
|
|
|
|
|
// -----------------------------
|
|
|
|
// Helper: Validate Ring Orientation
|
|
|
|
// -----------------------------
|
|
|
|
bool validate_orientation() { |
|
|
|
bool validate_orientation() |
|
|
|
{ |
|
|
|
// Test CCW (positive area)
|
|
|
|
PolygonRing ccw = {{0,0}, {1,0}, {1,1}, {0,1}}; |
|
|
|
PolygonRing ccw = { |
|
|
|
{0, 0}, |
|
|
|
{1, 0}, |
|
|
|
{1, 1}, |
|
|
|
{0, 1} |
|
|
|
}; |
|
|
|
if (compute_ring_area(ccw) <= 0) { |
|
|
|
std::cerr << "Error: CCW ring has non-positive area!\n"; |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
// Test CW (negative area)
|
|
|
|
PolygonRing cw = {{0,0}, {0,1}, {1,1}, {1,0}}; |
|
|
|
PolygonRing cw = { |
|
|
|
{0, 0}, |
|
|
|
{0, 1}, |
|
|
|
{1, 1}, |
|
|
|
{1, 0} |
|
|
|
}; |
|
|
|
if (compute_ring_area(cw) >= 0) { |
|
|
|
std::cerr << "Error: CW ring has non-negative area!\n"; |
|
|
|
return false; |
|
|
|
@ -49,7 +56,8 @@ bool validate_orientation() { |
|
|
|
// -----------------------------
|
|
|
|
// Validate Test Case: Orientation of outer ring (CCW) and holes (CW)
|
|
|
|
// -----------------------------
|
|
|
|
bool validate_test_case(const TestCase& tc) { |
|
|
|
bool validate_test_case(const TestCase& tc) |
|
|
|
{ |
|
|
|
bool valid = true; |
|
|
|
std::string status = ""; |
|
|
|
|
|
|
|
@ -74,10 +82,8 @@ bool validate_test_case(const TestCase& tc) { |
|
|
|
|
|
|
|
std::cout << "[" |
|
|
|
<< "\033[33mINVALID\033[0m" // make "INVALID" yellow
|
|
|
|
<< "] " |
|
|
|
<< tc.name << "\n" |
|
|
|
<< " Query: " << tc.query_point |
|
|
|
<< " | Status: \033[33mInvalid Test Case\033[0m" |
|
|
|
<< "] " << tc.name << "\n" |
|
|
|
<< " Query: " << tc.query_point << " | Status: \033[33mInvalid Test Case\033[0m" |
|
|
|
<< " | Reason: " << (valid ? "OK" : status) << "\n"; |
|
|
|
|
|
|
|
return valid; |
|
|
|
@ -86,16 +92,13 @@ bool validate_test_case(const TestCase& tc) { |
|
|
|
// -----------------------------
|
|
|
|
// Run Single Test
|
|
|
|
// -----------------------------
|
|
|
|
bool run_test(const TestCase& tc) { |
|
|
|
bool run_test(const TestCase& tc) |
|
|
|
{ |
|
|
|
bool result = point_in_polygon_with_holes(tc.query_point, tc.outer_ring, tc.holes); |
|
|
|
bool passed = (result == tc.expected); |
|
|
|
|
|
|
|
std::cout << "[" |
|
|
|
<< (passed ? "\033[32mPASS\033[0m" : "\033[31mFAIL\033[0m") |
|
|
|
<< "] " |
|
|
|
<< tc.name << "\n" |
|
|
|
<< " Query: " << tc.query_point |
|
|
|
<< " | Expected: " << (tc.expected ? "Inside" : "Outside") |
|
|
|
std::cout << "[" << (passed ? "\033[32mPASS\033[0m" : "\033[31mFAIL\033[0m") << "] " << tc.name << "\n" |
|
|
|
<< " Query: " << tc.query_point << " | Expected: " << (tc.expected ? "Inside" : "Outside") |
|
|
|
<< " | Got: " << (result ? "Inside" : "Outside") << "\n"; |
|
|
|
|
|
|
|
return passed; |
|
|
|
@ -105,19 +108,32 @@ bool run_test(const TestCase& tc) { |
|
|
|
// Test Case Factory Functions
|
|
|
|
// -----------------------------
|
|
|
|
|
|
|
|
std::vector<TestCase> create_test_cases() { |
|
|
|
std::vector<TestCase> create_test_cases() |
|
|
|
{ |
|
|
|
std::vector<TestCase> tests; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// --------------------------------------------------
|
|
|
|
// Test 1: Simple hole - point inside hole → Outside
|
|
|
|
// --------------------------------------------------
|
|
|
|
PolygonRing outer1 = {{0,0}, {2,0}, {2,2}, {0,2}}; |
|
|
|
PolygonRing hole1 = {{0.5,0.5}, {0.5,1.5}, {1.5,1.5}, {1.5,0.5}}; |
|
|
|
PolygonRing outer1 = { |
|
|
|
{0, 0}, |
|
|
|
{2, 0}, |
|
|
|
{2, 2}, |
|
|
|
{0, 2} |
|
|
|
}; |
|
|
|
PolygonRing hole1 = { |
|
|
|
{0.5, 0.5}, |
|
|
|
{0.5, 1.5}, |
|
|
|
{1.5, 1.5}, |
|
|
|
{1.5, 0.5} |
|
|
|
}; |
|
|
|
tests.push_back({ |
|
|
|
"Inside Hole", |
|
|
|
outer1, {hole1}, {1.0, 1.0}, false |
|
|
|
outer1, |
|
|
|
{hole1}, |
|
|
|
{1.0, 1.0}, |
|
|
|
false |
|
|
|
}); |
|
|
|
|
|
|
|
// --------------------------------------------------
|
|
|
|
@ -125,7 +141,10 @@ std::vector<TestCase> create_test_cases() { |
|
|
|
// --------------------------------------------------
|
|
|
|
tests.push_back({ |
|
|
|
"Inside Outer, Outside Hole", |
|
|
|
outer1, {hole1}, {0.1, 0.1}, true |
|
|
|
outer1, |
|
|
|
{hole1}, |
|
|
|
{0.1, 0.1}, |
|
|
|
true |
|
|
|
}); |
|
|
|
|
|
|
|
// --------------------------------------------------
|
|
|
|
@ -133,7 +152,10 @@ std::vector<TestCase> create_test_cases() { |
|
|
|
// --------------------------------------------------
|
|
|
|
tests.push_back({ |
|
|
|
"Outside Outer", |
|
|
|
outer1, {hole1}, {3.0, 3.0}, false |
|
|
|
outer1, |
|
|
|
{hole1}, |
|
|
|
{3.0, 3.0}, |
|
|
|
false |
|
|
|
}); |
|
|
|
|
|
|
|
// --------------------------------------------------
|
|
|
|
@ -142,7 +164,10 @@ std::vector<TestCase> create_test_cases() { |
|
|
|
// --------------------------------------------------
|
|
|
|
tests.push_back({ |
|
|
|
"On Outer Edge (Bottom)", |
|
|
|
outer1, {}, {1.0, 0.0}, true |
|
|
|
outer1, |
|
|
|
{}, |
|
|
|
{1.0, 0.0}, |
|
|
|
true |
|
|
|
}); |
|
|
|
|
|
|
|
// --------------------------------------------------
|
|
|
|
@ -150,70 +175,138 @@ std::vector<TestCase> create_test_cases() { |
|
|
|
// --------------------------------------------------
|
|
|
|
tests.push_back({ |
|
|
|
"On Outer Vertex Offset Slightly Outside", |
|
|
|
outer1, {}, {0.0, 0.0-1e-8}, false |
|
|
|
outer1, |
|
|
|
{}, |
|
|
|
{0.0, 0.0 - 1e-8}, |
|
|
|
false |
|
|
|
}); |
|
|
|
|
|
|
|
// --------------------------------------------------
|
|
|
|
// Test 6: Point on hole edge
|
|
|
|
// Should be outside (on hole boundary still "outside" the solid)
|
|
|
|
// --------------------------------------------------
|
|
|
|
PolygonRing hole6 = {{1.0,1.0}, {1.0,2.0}, {2.0,2.0}, {2.0,1.0}}; // CW
|
|
|
|
PolygonRing hole6 = { |
|
|
|
{1.0, 1.0}, |
|
|
|
{1.0, 2.0}, |
|
|
|
{2.0, 2.0}, |
|
|
|
{2.0, 1.0} |
|
|
|
}; // CW
|
|
|
|
tests.push_back({ |
|
|
|
"On Hole Edge", |
|
|
|
{{0,0},{3,0},{3,3},{0,3}}, {hole6}, {1.5, 1.0}, false |
|
|
|
{{0, 0}, {3, 0}, {3, 3}, {0, 3}}, |
|
|
|
{hole6}, |
|
|
|
{1.5, 1.0}, |
|
|
|
false |
|
|
|
}); |
|
|
|
|
|
|
|
// --------------------------------------------------
|
|
|
|
// Test 7: Multiple holes
|
|
|
|
// --------------------------------------------------
|
|
|
|
PolygonRing hole7a = {{0.5,0.5}, {0.5,1.0}, {1.0,1.0}, {1.0,0.5}}; // CW
|
|
|
|
PolygonRing hole7b = {{1.5,1.5}, {1.5,2.0}, {2.0,2.0}, {2.0,1.5}}; // CW
|
|
|
|
PolygonRing hole7a = { |
|
|
|
{0.5, 0.5}, |
|
|
|
{0.5, 1.0}, |
|
|
|
{1.0, 1.0}, |
|
|
|
{1.0, 0.5} |
|
|
|
}; // CW
|
|
|
|
PolygonRing hole7b = { |
|
|
|
{1.5, 1.5}, |
|
|
|
{1.5, 2.0}, |
|
|
|
{2.0, 2.0}, |
|
|
|
{2.0, 1.5} |
|
|
|
}; // CW
|
|
|
|
tests.push_back({ |
|
|
|
"Multiple Holes - Inside", |
|
|
|
outer1, {hole7a, hole7b}, {0.1, 0.1}, true |
|
|
|
outer1, |
|
|
|
{hole7a, hole7b}, |
|
|
|
{0.1, 0.1 }, |
|
|
|
true |
|
|
|
}); |
|
|
|
|
|
|
|
tests.push_back({ |
|
|
|
"Multiple Holes - In First Hole", |
|
|
|
outer1, {hole7a, hole7b}, {0.7, 0.7}, false |
|
|
|
outer1, |
|
|
|
{hole7a, hole7b}, |
|
|
|
{0.7, 0.7 }, |
|
|
|
false |
|
|
|
}); |
|
|
|
|
|
|
|
tests.push_back({ |
|
|
|
"Multiple Holes - In Second Hole", |
|
|
|
outer1, {hole7a, hole7b}, {1.7, 1.7}, false |
|
|
|
outer1, |
|
|
|
{hole7a, hole7b}, |
|
|
|
{1.7, 1.7 }, |
|
|
|
false |
|
|
|
}); |
|
|
|
|
|
|
|
// --------------------------------------------------
|
|
|
|
// Test 8: Nested holes (hole inside hole) - NOT standard, but test behavior
|
|
|
|
// Note: This is not valid in simple polygons, but test robustness
|
|
|
|
// --------------------------------------------------
|
|
|
|
PolygonRing outer8 = {{0,0}, {3,0}, {3,3}, {0,3}}; |
|
|
|
PolygonRing hole8a = {{0.5,0.5}, {0.5,2.5}, {2.5,2.5}, {2.5,0.5}}; // large hole
|
|
|
|
PolygonRing hole8b = {{1.0,1.0}, {1.0,2.0}, {2.0,2.0}, {2.0,1.0}}; // small hole inside hole8a
|
|
|
|
PolygonRing outer8 = { |
|
|
|
{0, 0}, |
|
|
|
{3, 0}, |
|
|
|
{3, 3}, |
|
|
|
{0, 3} |
|
|
|
}; |
|
|
|
PolygonRing hole8a = { |
|
|
|
{0.5, 0.5}, |
|
|
|
{0.5, 2.5}, |
|
|
|
{2.5, 2.5}, |
|
|
|
{2.5, 0.5} |
|
|
|
}; // large hole
|
|
|
|
PolygonRing hole8b = { |
|
|
|
{1.0, 1.0}, |
|
|
|
{1.0, 2.0}, |
|
|
|
{2.0, 2.0}, |
|
|
|
{2.0, 1.0} |
|
|
|
}; // small hole inside hole8a
|
|
|
|
// This creates a "island" in the hole
|
|
|
|
tests.push_back({ |
|
|
|
"Nested Holes - On Island", |
|
|
|
outer8, {hole8a, hole8b}, {1.5, 1.5}, true // inside outer, inside hole8a, but outside hole8b → solid
|
|
|
|
outer8, |
|
|
|
{hole8a, hole8b}, |
|
|
|
{1.5, 1.5 }, |
|
|
|
true // inside outer, inside hole8a, but outside hole8b → solid
|
|
|
|
}); |
|
|
|
|
|
|
|
// --------------------------------------------------
|
|
|
|
// Test 9: Complex L-shaped polygon with hole
|
|
|
|
// --------------------------------------------------
|
|
|
|
PolygonRing outer9 = { |
|
|
|
{0,0}, {3,0}, {3,1}, {2,1}, {2,3}, {0,3}, {0,0} // L-shape, explicitly closed and no self-intersection
|
|
|
|
{0, 0}, |
|
|
|
{3, 0}, |
|
|
|
{3, 1}, |
|
|
|
{2, 1}, |
|
|
|
{2, 3}, |
|
|
|
{0, 3}, |
|
|
|
{0, 0} // L-shape, explicitly closed and no self-intersection
|
|
|
|
}; // CCW
|
|
|
|
PolygonRing hole9 = {{0.5,0.5}, {0.5,1.5}, {1.5,1.5}, {1.5,0.5}}; // CW
|
|
|
|
PolygonRing hole9 = { |
|
|
|
{0.5, 0.5}, |
|
|
|
{0.5, 1.5}, |
|
|
|
{1.5, 1.5}, |
|
|
|
{1.5, 0.5} |
|
|
|
}; // CW
|
|
|
|
tests.push_back({ |
|
|
|
"L-Shape with Hole - Inside Leg", |
|
|
|
outer9, {hole9}, {0.1, 0.1}, true |
|
|
|
outer9, |
|
|
|
{hole9}, |
|
|
|
{0.1, 0.1}, |
|
|
|
true |
|
|
|
}); |
|
|
|
tests.push_back({ |
|
|
|
"L-Shape with Hole - In Hole", |
|
|
|
outer9, {hole9}, {1.0, 1.0}, false |
|
|
|
outer9, |
|
|
|
{hole9}, |
|
|
|
{1.0, 1.0}, |
|
|
|
false |
|
|
|
}); |
|
|
|
tests.push_back({ |
|
|
|
"L-Shape with Hole - In Arm", |
|
|
|
outer9, {hole9}, {2.0, 0.5}, true |
|
|
|
outer9, |
|
|
|
{hole9}, |
|
|
|
{2.0, 0.5}, |
|
|
|
true |
|
|
|
}); |
|
|
|
|
|
|
|
// --------------------------------------------------
|
|
|
|
@ -221,7 +314,10 @@ std::vector<TestCase> create_test_cases() { |
|
|
|
// --------------------------------------------------
|
|
|
|
tests.push_back({ |
|
|
|
"Near Degenerate - Close to Corner", |
|
|
|
outer1, {hole1}, {0.5000001, 0.5000001}, false // just outside hole
|
|
|
|
outer1, |
|
|
|
{hole1}, |
|
|
|
{0.5000001, 0.5000001}, |
|
|
|
false // just outside hole
|
|
|
|
}); |
|
|
|
|
|
|
|
return tests; |
|
|
|
@ -230,7 +326,8 @@ std::vector<TestCase> create_test_cases() { |
|
|
|
// -----------------------------
|
|
|
|
// Main Function
|
|
|
|
// -----------------------------
|
|
|
|
int main() { |
|
|
|
int main() |
|
|
|
{ |
|
|
|
std::cout << "Running Polygon Winding Number Tests...\n\n"; |
|
|
|
|
|
|
|
// Optional: Validate orientation logic
|
|
|
|
@ -245,12 +342,8 @@ int main() { |
|
|
|
int total = test_cases.size(); |
|
|
|
|
|
|
|
for (const auto& tc : test_cases) { |
|
|
|
if (!validate_test_case(tc)) { |
|
|
|
continue; |
|
|
|
} |
|
|
|
if (run_test(tc)) { |
|
|
|
passed++; |
|
|
|
} |
|
|
|
if (!validate_test_case(tc)) { continue; } |
|
|
|
if (run_test(tc)) { passed++; } |
|
|
|
} |
|
|
|
|
|
|
|
std::cout << "\nSummary: " << passed << " / " << total << " tests passed.\n"; |
|
|
|
|