extract explicit mesh with topology information from implicit surfaces with boolean operations, and do surface/volume integrating on them.
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.
 
 
 
 
 
 

358 lines
9.3 KiB

// 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;
}
}