|
|
|
|
// main.cpp
|
|
|
|
|
#include "polygon_winding_number.h"
|
|
|
|
|
#include <iostream>
|
|
|
|
|
#include <vector>
|
|
|
|
|
#include <string>
|
|
|
|
|
#include <cassert>
|
|
|
|
|
|
|
|
|
|
// -----------------------------
|
|
|
|
|
// Test Case Definition
|
|
|
|
|
// -----------------------------
|
|
|
|
|
|
|
|
|
|
struct TestCase {
|
|
|
|
|
std::string name;
|
|
|
|
|
PolygonRing outer_ring;
|
|
|
|
|
std::vector<PolygonRing> holes;
|
|
|
|
|
Point2d query_point;
|
|
|
|
|
bool expected; // true = inside, false = outside
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// -----------------------------
|
|
|
|
|
// Helper: Print Point
|
|
|
|
|
// -----------------------------
|
|
|
|
|
std::ostream& operator<<(std::ostream& os, const Point2d& p) { return os << "(" << p.x() << ", " << p.y() << ")"; }
|
|
|
|
|
|
|
|
|
|
// -----------------------------
|
|
|
|
|
// Helper: Validate Ring Orientation
|
|
|
|
|
// -----------------------------
|
|
|
|
|
bool validate_orientation()
|
|
|
|
|
{
|
|
|
|
|
// Test CCW (positive area)
|
|
|
|
|
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}
|
|
|
|
|
};
|
|
|
|
|
if (compute_ring_area(cw) >= 0) {
|
|
|
|
|
std::cerr << "Error: CW ring has non-negative area!\n";
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// -----------------------------
|
|
|
|
|
// Validate Test Case: Orientation of outer ring (CCW) and holes (CW)
|
|
|
|
|
// -----------------------------
|
|
|
|
|
bool validate_test_case(const TestCase& tc)
|
|
|
|
|
{
|
|
|
|
|
bool valid = true;
|
|
|
|
|
std::string status = "";
|
|
|
|
|
|
|
|
|
|
// Validate outer ring orientation (should be CCW → positive area)
|
|
|
|
|
double outer_area = compute_ring_area(tc.outer_ring);
|
|
|
|
|
if (outer_area <= 0) {
|
|
|
|
|
status += "Outer ring not CCW (area = " + std::to_string(outer_area) + ")";
|
|
|
|
|
valid = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate holes orientation (should be CW → negative area)
|
|
|
|
|
for (size_t i = 0; i < tc.holes.size(); ++i) {
|
|
|
|
|
double hole_area = compute_ring_area(tc.holes[i]);
|
|
|
|
|
if (hole_area >= 0) {
|
|
|
|
|
if (!status.empty()) status += ", ";
|
|
|
|
|
status += "Hole " + std::to_string(i) + " not CW (area = " + std::to_string(hole_area) + ")";
|
|
|
|
|
valid = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (valid) return true;
|
|
|
|
|
|
|
|
|
|
std::cout << "["
|
|
|
|
|
<< "\033[33mINVALID\033[0m" // make "INVALID" yellow
|
|
|
|
|
<< "] " << tc.name << "\n"
|
|
|
|
|
<< " Query: " << tc.query_point << " | Status: \033[33mInvalid Test Case\033[0m"
|
|
|
|
|
<< " | Reason: " << (valid ? "OK" : status) << "\n";
|
|
|
|
|
|
|
|
|
|
return valid;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// -----------------------------
|
|
|
|
|
// Run Single Test
|
|
|
|
|
// -----------------------------
|
|
|
|
|
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")
|
|
|
|
|
<< " | Got: " << (result ? "Inside" : "Outside") << "\n";
|
|
|
|
|
|
|
|
|
|
return passed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// -----------------------------
|
|
|
|
|
// Test Case Factory Functions
|
|
|
|
|
// -----------------------------
|
|
|
|
|
|
|
|
|
|
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}
|
|
|
|
|
};
|
|
|
|
|
tests.push_back({
|
|
|
|
|
"Inside Hole",
|
|
|
|
|
outer1,
|
|
|
|
|
{hole1},
|
|
|
|
|
{1.0, 1.0},
|
|
|
|
|
false
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// --------------------------------------------------
|
|
|
|
|
// Test 2: Point inside outer, outside all holes → Inside
|
|
|
|
|
// --------------------------------------------------
|
|
|
|
|
tests.push_back({
|
|
|
|
|
"Inside Outer, Outside Hole",
|
|
|
|
|
outer1,
|
|
|
|
|
{hole1},
|
|
|
|
|
{0.1, 0.1},
|
|
|
|
|
true
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// --------------------------------------------------
|
|
|
|
|
// Test 3: Point outside outer ring → Outside
|
|
|
|
|
// --------------------------------------------------
|
|
|
|
|
tests.push_back({
|
|
|
|
|
"Outside Outer",
|
|
|
|
|
outer1,
|
|
|
|
|
{hole1},
|
|
|
|
|
{3.0, 3.0},
|
|
|
|
|
false
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// --------------------------------------------------
|
|
|
|
|
// Test 4: Point on outer ring edge (bottom edge)
|
|
|
|
|
// Winding rule: on boundary → considered inside
|
|
|
|
|
// --------------------------------------------------
|
|
|
|
|
tests.push_back({
|
|
|
|
|
"On Outer Edge (Bottom)",
|
|
|
|
|
outer1,
|
|
|
|
|
{},
|
|
|
|
|
{1.0, 0.0},
|
|
|
|
|
true
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// --------------------------------------------------
|
|
|
|
|
// Test 5: Point on outer ring vertex
|
|
|
|
|
// --------------------------------------------------
|
|
|
|
|
tests.push_back({
|
|
|
|
|
"On Outer Vertex Offset Slightly Outside",
|
|
|
|
|
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
|
|
|
|
|
tests.push_back({
|
|
|
|
|
"On Hole Edge",
|
|
|
|
|
{{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
|
|
|
|
|
tests.push_back({
|
|
|
|
|
"Multiple Holes - Inside",
|
|
|
|
|
outer1,
|
|
|
|
|
{hole7a, hole7b},
|
|
|
|
|
{0.1, 0.1 },
|
|
|
|
|
true
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
tests.push_back({
|
|
|
|
|
"Multiple Holes - In First Hole",
|
|
|
|
|
outer1,
|
|
|
|
|
{hole7a, hole7b},
|
|
|
|
|
{0.7, 0.7 },
|
|
|
|
|
false
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
tests.push_back({
|
|
|
|
|
"Multiple Holes - In Second Hole",
|
|
|
|
|
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
|
|
|
|
|
// 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
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// --------------------------------------------------
|
|
|
|
|
// 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
|
|
|
|
|
}; // CCW
|
|
|
|
|
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
|
|
|
|
|
});
|
|
|
|
|
tests.push_back({
|
|
|
|
|
"L-Shape with Hole - In Hole",
|
|
|
|
|
outer9,
|
|
|
|
|
{hole9},
|
|
|
|
|
{1.0, 1.0},
|
|
|
|
|
false
|
|
|
|
|
});
|
|
|
|
|
tests.push_back({
|
|
|
|
|
"L-Shape with Hole - In Arm",
|
|
|
|
|
outer9,
|
|
|
|
|
{hole9},
|
|
|
|
|
{2.0, 0.5},
|
|
|
|
|
true
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// --------------------------------------------------
|
|
|
|
|
// Test 10: Degenerate: Point exactly at intersection of hole and outer? (not possible, but test near)
|
|
|
|
|
// --------------------------------------------------
|
|
|
|
|
tests.push_back({
|
|
|
|
|
"Near Degenerate - Close to Corner",
|
|
|
|
|
outer1,
|
|
|
|
|
{hole1},
|
|
|
|
|
{0.5000001, 0.5000001},
|
|
|
|
|
false // just outside hole
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return tests;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// -----------------------------
|
|
|
|
|
// Main Function
|
|
|
|
|
// -----------------------------
|
|
|
|
|
int main()
|
|
|
|
|
{
|
|
|
|
|
std::cout << "Running Polygon Winding Number Tests...\n\n";
|
|
|
|
|
|
|
|
|
|
// Optional: Validate orientation logic
|
|
|
|
|
if (!validate_orientation()) {
|
|
|
|
|
std::cerr << "Orientation validation failed!\n";
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto test_cases = create_test_cases();
|
|
|
|
|
|
|
|
|
|
int passed = 0;
|
|
|
|
|
int total = test_cases.size();
|
|
|
|
|
|
|
|
|
|
for (const auto& tc : test_cases) {
|
|
|
|
|
if (!validate_test_case(tc)) { continue; }
|
|
|
|
|
if (run_test(tc)) { passed++; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::cout << "\nSummary: " << passed << " / " << total << " tests passed.\n";
|
|
|
|
|
|
|
|
|
|
if (passed == total) {
|
|
|
|
|
std::cout << "\033[32m All tests passed!\033[0m\n";
|
|
|
|
|
return 0;
|
|
|
|
|
} else {
|
|
|
|
|
std::cout << "\033[31m Some tests failed!\033[0m\n";
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
}
|