You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
265 lines
8.5 KiB
265 lines
8.5 KiB
|
2 weeks ago
|
// 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;
|
||
|
|
}
|
||
|
|
}
|