// main.cpp #include "polygon_winding_number.h" #include #include #include #include // ----------------------------- // Test Case Definition // ----------------------------- struct TestCase { std::string name; PolygonRing outer_ring; std::vector 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 create_test_cases() { std::vector 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; } }