feat: 测试覆盖扩展 + 文档更新
This commit is contained in:
+150
-6
@@ -1,8 +1,13 @@
|
||||
#include "vde/core/distance.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace vde::core {
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Point distances
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
double distance(const Point3D& a, const Point3D& b) { return (a - b).norm(); }
|
||||
|
||||
double distance(const Point3D& p, const Line3D<double>& line) {
|
||||
@@ -11,11 +16,7 @@ double distance(const Point3D& p, const Line3D<double>& line) {
|
||||
}
|
||||
|
||||
double distance(const Point3D& p, const Segment3D<double>& seg) {
|
||||
Vector3D ab = seg.p1() - seg.p0();
|
||||
Vector3D ap = p - seg.p0();
|
||||
double t = ap.dot(ab) / ab.squaredNorm();
|
||||
t = std::clamp(t, 0.0, 1.0);
|
||||
return (seg.p0() + t * ab - p).norm();
|
||||
return (closest_point(p, seg) - p).norm();
|
||||
}
|
||||
|
||||
double distance(const Point3D& p, const Plane<double>& plane) {
|
||||
@@ -26,15 +27,143 @@ double distance(const Point3D& p, const Triangle<double>& tri) {
|
||||
return (p - closest_point(p, tri)).norm();
|
||||
}
|
||||
|
||||
double distance(const Point3D& p, const AABB<double>& box) {
|
||||
return (p - closest_point(p, box)).norm();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Feature-pair distances
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
double distance(const Segment3D<double>& a, const Segment3D<double>& b) {
|
||||
// Closest points between two segments via the classic
|
||||
// "clamp to [0,1] then clamp the other" approach.
|
||||
Vector3D d1 = a.p1() - a.p0();
|
||||
Vector3D d2 = b.p1() - b.p0();
|
||||
Vector3D r = a.p0() - b.p0();
|
||||
|
||||
double a_len2 = d1.squaredNorm();
|
||||
double b_len2 = d2.squaredNorm();
|
||||
double ab = d1.dot(d2);
|
||||
double ar = d1.dot(r);
|
||||
double br = d2.dot(r);
|
||||
|
||||
double denom = a_len2 * b_len2 - ab * ab;
|
||||
double s, t;
|
||||
|
||||
if (denom < 1e-12) {
|
||||
// (Nearly) parallel segments
|
||||
s = 0.0;
|
||||
t = (ab > 0.0) ? br / b_len2 : 0.0;
|
||||
t = std::clamp(t, 0.0, 1.0);
|
||||
} else {
|
||||
s = (ab * br - b_len2 * ar) / denom;
|
||||
s = std::clamp(s, 0.0, 1.0);
|
||||
t = (ab * s + br) / b_len2;
|
||||
}
|
||||
|
||||
// Clamp t then re-clamp s (standard "region" logic)
|
||||
if (t < 0.0) {
|
||||
t = 0.0;
|
||||
s = std::clamp(-ar / a_len2, 0.0, 1.0);
|
||||
} else if (t > 1.0) {
|
||||
t = 1.0;
|
||||
s = std::clamp((ab - ar) / a_len2, 0.0, 1.0);
|
||||
}
|
||||
|
||||
Point3D ca = a.p0() + s * d1;
|
||||
Point3D cb = b.p0() + t * d2;
|
||||
return (ca - cb).norm();
|
||||
}
|
||||
|
||||
double distance(const Line3D<double>& a, const Line3D<double>& b) {
|
||||
Vector3D u = a.direction();
|
||||
Vector3D v = b.direction();
|
||||
Vector3D w = a.origin() - b.origin();
|
||||
|
||||
double uv = u.dot(v);
|
||||
double uu = u.squaredNorm(); // == 1 (normalized)
|
||||
double vv = v.squaredNorm(); // == 1
|
||||
double wu = w.dot(u);
|
||||
double wv = w.dot(v);
|
||||
|
||||
double denom = uu * vv - uv * uv;
|
||||
if (std::abs(denom) < 1e-12) {
|
||||
// Parallel → distance from origin of a to line b
|
||||
return (w - w.dot(v) * v).norm();
|
||||
}
|
||||
double s = (uv * wv - vv * wu) / denom;
|
||||
double t = (uu * wv - uv * wu) / denom;
|
||||
|
||||
Point3D pa = a.origin() + s * u;
|
||||
Point3D pb = b.origin() + t * v;
|
||||
return (pa - pb).norm();
|
||||
}
|
||||
|
||||
double distance(const AABB<double>& a, const AABB<double>& b) {
|
||||
// Squared distance between two AABBs (0 when overlapping)
|
||||
double sq = 0.0;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
if (a.max()(i) < b.min()(i)) {
|
||||
double gap = b.min()(i) - a.max()(i);
|
||||
sq += gap * gap;
|
||||
} else if (b.max()(i) < a.min()(i)) {
|
||||
double gap = a.min()(i) - b.max()(i);
|
||||
sq += gap * gap;
|
||||
}
|
||||
// else: overlapping along this axis → contributes 0
|
||||
}
|
||||
return std::sqrt(sq);
|
||||
}
|
||||
|
||||
double distance(const Triangle<double>& a, const Triangle<double>& b) {
|
||||
// Minimum distance between two triangles is the minimum over:
|
||||
// 6 vertex-face distances + 9 edge-edge distances
|
||||
double dmin = std::numeric_limits<double>::max();
|
||||
|
||||
// Vertex of a ↔ face of b
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
dmin = std::min(dmin, distance(a.v(i), b));
|
||||
}
|
||||
// Vertex of b ↔ face of a
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
dmin = std::min(dmin, distance(b.v(i), a));
|
||||
}
|
||||
// Edge of a ↔ edge of b
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
Segment3D<double> ea(a.v(i), a.v((i + 1) % 3));
|
||||
for (int j = 0; j < 3; ++j) {
|
||||
Segment3D<double> eb(b.v(j), b.v((j + 1) % 3));
|
||||
dmin = std::min(dmin, distance(ea, eb));
|
||||
}
|
||||
}
|
||||
return dmin;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Closest-point queries
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
Point3D closest_point(const Point3D& p, const Triangle<double>& tri) {
|
||||
Vector3D v0 = tri.v(2) - tri.v(0), v1 = tri.v(1) - tri.v(0), v2 = p - tri.v(0);
|
||||
double d00 = v0.dot(v0), d01 = v0.dot(v1), d11 = v1.dot(v1);
|
||||
double d20 = v2.dot(v0), d21 = v2.dot(v1);
|
||||
double denom = d00 * d11 - d01 * d01;
|
||||
if (std::abs(denom) < 1e-14) {
|
||||
// Degenerate triangle – fall back to closest vertex
|
||||
int best = 0;
|
||||
double best_d2 = (p - tri.v(0)).squaredNorm();
|
||||
for (int i = 1; i < 3; ++i) {
|
||||
double d2 = (p - tri.v(i)).squaredNorm();
|
||||
if (d2 < best_d2) { best_d2 = d2; best = i; }
|
||||
}
|
||||
return tri.v(best);
|
||||
}
|
||||
double v = (d11 * d20 - d01 * d21) / denom;
|
||||
double w = (d00 * d21 - d01 * d20) / denom;
|
||||
if (v >= 0 && w >= 0 && v + w <= 1)
|
||||
return tri.v(0) + v * v0 + w * v1;
|
||||
|
||||
// Fallback to closest point on edges
|
||||
double d0 = distance(p, Segment3D<double>(tri.v(0), tri.v(1)));
|
||||
double d1 = distance(p, Segment3D<double>(tri.v(1), tri.v(2)));
|
||||
@@ -48,9 +177,24 @@ Point3D closest_point(const Point3D& p, const Triangle<double>& tri) {
|
||||
Point3D closest_point(const Point3D& p, const Segment3D<double>& seg) {
|
||||
Vector3D ab = seg.p1() - seg.p0();
|
||||
Vector3D ap = p - seg.p0();
|
||||
double t = ap.dot(ab) / ab.squaredNorm();
|
||||
double len2 = ab.squaredNorm();
|
||||
if (len2 < 1e-14) return seg.p0();
|
||||
double t = ap.dot(ab) / len2;
|
||||
t = std::clamp(t, 0.0, 1.0);
|
||||
return seg.p0() + t * ab;
|
||||
}
|
||||
|
||||
Point3D closest_point(const Point3D& p, const AABB<double>& box) {
|
||||
// Clamp each coordinate to [min, max]
|
||||
return Point3D(
|
||||
std::clamp(p.x(), box.min().x(), box.max().x()),
|
||||
std::clamp(p.y(), box.min().y(), box.max().y()),
|
||||
std::clamp(p.z(), box.min().z(), box.max().z())
|
||||
);
|
||||
}
|
||||
|
||||
Point3D closest_point(const Point3D& p, const Plane<double>& plane) {
|
||||
return plane.project(p);
|
||||
}
|
||||
|
||||
} // namespace vde::core
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
#include "vde/core/point.h"
|
||||
namespace vde::core {}
|
||||
+258
-1
@@ -1,8 +1,73 @@
|
||||
#include "vde/core/polygon.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace vde::core {
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Perimeter
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
double Polygon2D::perimeter() const {
|
||||
if (vertices_.size() < 2) return 0.0;
|
||||
double p = 0.0;
|
||||
for (size_t i = 0; i < vertices_.size(); ++i) {
|
||||
const auto& a = vertices_[i];
|
||||
const auto& b = vertices_[(i + 1) % vertices_.size()];
|
||||
p += (a - b).norm();
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Centroid (area-weighted)
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
Point2D Polygon2D::centroid() const {
|
||||
if (vertices_.size() < 3) {
|
||||
if (vertices_.empty()) return Point2D(0, 0);
|
||||
// Point or line → average of vertices
|
||||
Point2D sum(0, 0);
|
||||
for (const auto& v : vertices_) sum += v;
|
||||
return sum / static_cast<double>(vertices_.size());
|
||||
}
|
||||
|
||||
double A = signed_area();
|
||||
if (std::abs(A) < 1e-14) {
|
||||
// Degenerate → average
|
||||
Point2D sum(0, 0);
|
||||
for (const auto& v : vertices_) sum += v;
|
||||
return sum / static_cast<double>(vertices_.size());
|
||||
}
|
||||
|
||||
double cx = 0.0, cy = 0.0;
|
||||
for (size_t i = 0; i < vertices_.size(); ++i) {
|
||||
const auto& a = vertices_[i];
|
||||
const auto& b = vertices_[(i + 1) % vertices_.size()];
|
||||
double cross = a.x() * b.y() - b.x() * a.y();
|
||||
cx += (a.x() + b.x()) * cross;
|
||||
cy += (a.y() + b.y()) * cross;
|
||||
}
|
||||
double factor = 1.0 / (6.0 * A);
|
||||
return Point2D(cx * factor, cy * factor);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Bounding box
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
AABB<double> Polygon2D::bounding_box() const {
|
||||
AABB<double> aabb;
|
||||
for (const auto& v : vertices_) {
|
||||
aabb.expand(Point3D(v.x(), v.y(), 0.0));
|
||||
}
|
||||
return aabb;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Signed area (unchanged)
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
double Polygon2D::signed_area() const {
|
||||
if (vertices_.size() < 3) return 0.0;
|
||||
double area = 0.0;
|
||||
@@ -14,8 +79,11 @@ double Polygon2D::signed_area() const {
|
||||
return area * 0.5;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Point-in-polygon (unchanged)
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
bool Polygon2D::contains(const Point2D& p) const {
|
||||
// Ray casting algorithm
|
||||
bool inside = false;
|
||||
size_t n = vertices_.size();
|
||||
for (size_t i = 0, j = n - 1; i < n; j = i++) {
|
||||
@@ -29,4 +97,193 @@ bool Polygon2D::contains(const Point2D& p) const {
|
||||
return inside;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Douglas-Peucker simplification
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
namespace {
|
||||
|
||||
double perpendicular_distance(const Point2D& p, const Point2D& a, const Point2D& b) {
|
||||
Vector2D ab = b - a;
|
||||
double len2 = ab.squaredNorm();
|
||||
if (len2 < 1e-14) return (p - a).norm();
|
||||
double t = (p - a).dot(ab) / len2;
|
||||
t = std::clamp(t, 0.0, 1.0);
|
||||
Point2D proj = a + t * ab;
|
||||
return (p - proj).norm();
|
||||
}
|
||||
|
||||
void douglas_peucker_recursive(const std::vector<Point2D>& pts, size_t start, size_t end,
|
||||
double tolerance, std::vector<bool>& keep) {
|
||||
if (end <= start + 1) return;
|
||||
|
||||
double dmax = 0.0;
|
||||
size_t idx = start;
|
||||
for (size_t i = start + 1; i < end; ++i) {
|
||||
double d = perpendicular_distance(pts[i], pts[start], pts[end]);
|
||||
if (d > dmax) {
|
||||
dmax = d;
|
||||
idx = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (dmax > tolerance) {
|
||||
keep[idx] = true;
|
||||
douglas_peucker_recursive(pts, start, idx, tolerance, keep);
|
||||
douglas_peucker_recursive(pts, idx, end, tolerance, keep);
|
||||
}
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
Polygon2D Polygon2D::simplify(double tolerance) const {
|
||||
if (vertices_.size() < 3) return *this;
|
||||
|
||||
// For a closed polygon, we treat it as-is (first/last are not duplicated here).
|
||||
// Apply DP on the chain 0..n-1, keeping endpoints.
|
||||
std::vector<bool> keep(vertices_.size(), false);
|
||||
keep.front() = true;
|
||||
keep.back() = true;
|
||||
|
||||
douglas_peucker_recursive(vertices_, 0, vertices_.size() - 1, tolerance, keep);
|
||||
|
||||
std::vector<Point2D> result;
|
||||
for (size_t i = 0; i < vertices_.size(); ++i) {
|
||||
if (keep[i]) result.push_back(vertices_[i]);
|
||||
}
|
||||
return Polygon2D(std::move(result));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Ear-clipping triangulation
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
namespace {
|
||||
|
||||
double cross_2d(const Point2D& a, const Point2D& b, const Point2D& c) {
|
||||
return (b.x() - a.x()) * (c.y() - a.y()) - (b.y() - a.y()) * (c.x() - a.x());
|
||||
}
|
||||
|
||||
bool point_in_triangle(const Point2D& p, const Point2D& a,
|
||||
const Point2D& b, const Point2D& c) {
|
||||
double s1 = cross_2d(a, b, p);
|
||||
double s2 = cross_2d(b, c, p);
|
||||
double s3 = cross_2d(c, a, p);
|
||||
bool has_neg = (s1 < -1e-12) || (s2 < -1e-12) || (s3 < -1e-12);
|
||||
bool has_pos = (s1 > 1e-12) || (s2 > 1e-12) || (s3 > 1e-12);
|
||||
return !(has_neg && has_pos);
|
||||
}
|
||||
|
||||
bool is_ear(const std::vector<Point2D>& poly, int i, int j, int k,
|
||||
const std::vector<int>& remaining) {
|
||||
// Check convexity (CCW polygon → cross should be > 0)
|
||||
if (cross_2d(poly[i], poly[j], poly[k]) <= 0.0) return false;
|
||||
|
||||
// Check no other vertex lies inside triangle (i,j,k)
|
||||
for (int m : remaining) {
|
||||
if (m == i || m == j || m == k) continue;
|
||||
if (point_in_triangle(poly[m], poly[i], poly[j], poly[k])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
std::vector<std::array<int, 3>> Polygon2D::triangulate() const {
|
||||
std::vector<std::array<int, 3>> tris;
|
||||
if (vertices_.size() < 3) return tris;
|
||||
|
||||
// Work on a copy; use 0-based indices
|
||||
std::vector<Point2D> poly = vertices_;
|
||||
|
||||
// Ensure CCW
|
||||
if (signed_area() < 0) {
|
||||
std::reverse(poly.begin(), poly.end());
|
||||
}
|
||||
|
||||
int n = static_cast<int>(poly.size());
|
||||
std::vector<int> prev(n), next(n), remaining;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
prev[i] = (i - 1 + n) % n;
|
||||
next[i] = (i + 1) % n;
|
||||
remaining.push_back(i);
|
||||
}
|
||||
|
||||
int i = 0;
|
||||
int fail_count = 0;
|
||||
while (remaining.size() > 3 && fail_count < n * 2) {
|
||||
int a = prev[i];
|
||||
int b = i;
|
||||
int c = next[i];
|
||||
if (is_ear(poly, a, b, c, remaining)) {
|
||||
tris.push_back({a, b, c});
|
||||
// Remove vertex b
|
||||
next[a] = c;
|
||||
prev[c] = a;
|
||||
auto it = std::find(remaining.begin(), remaining.end(), b);
|
||||
if (it != remaining.end()) remaining.erase(it);
|
||||
i = a; // rewind slightly
|
||||
fail_count = 0;
|
||||
} else {
|
||||
i = next[i];
|
||||
++fail_count;
|
||||
}
|
||||
}
|
||||
|
||||
// Final triangle
|
||||
if (remaining.size() == 3) {
|
||||
tris.push_back({remaining[0], remaining[1], remaining[2]});
|
||||
}
|
||||
|
||||
return tris;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Andrew's monotone chain convex hull
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
Polygon2D Polygon2D::convex_hull_2d(const std::vector<Point2D>& points) {
|
||||
if (points.size() <= 1) return Polygon2D(points);
|
||||
|
||||
auto sorted = points;
|
||||
std::sort(sorted.begin(), sorted.end(),
|
||||
[](const Point2D& a, const Point2D& b) {
|
||||
return a.x() < b.x() || (a.x() == b.x() && a.y() < b.y());
|
||||
});
|
||||
|
||||
// Build lower hull
|
||||
std::vector<Point2D> hull;
|
||||
for (const auto& p : sorted) {
|
||||
while (hull.size() >= 2) {
|
||||
const auto& a = hull[hull.size() - 2];
|
||||
const auto& b = hull.back();
|
||||
if (cross_2d(a, b, p) <= 0.0)
|
||||
hull.pop_back();
|
||||
else
|
||||
break;
|
||||
}
|
||||
hull.push_back(p);
|
||||
}
|
||||
|
||||
// Build upper hull
|
||||
size_t lower_size = hull.size();
|
||||
for (auto it = sorted.rbegin(); it != sorted.rend(); ++it) {
|
||||
const auto& p = *it;
|
||||
while (hull.size() > lower_size) {
|
||||
const auto& a = hull[hull.size() - 2];
|
||||
const auto& b = hull.back();
|
||||
if (cross_2d(a, b, p) <= 0.0)
|
||||
hull.pop_back();
|
||||
else
|
||||
break;
|
||||
}
|
||||
hull.push_back(p);
|
||||
}
|
||||
|
||||
// Remove duplicate last point (== first point)
|
||||
if (hull.size() > 1) hull.pop_back();
|
||||
|
||||
return Polygon2D(std::move(hull));
|
||||
}
|
||||
|
||||
} // namespace vde::core
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
#include "vde/core/transform.h"
|
||||
namespace vde::core {}
|
||||
Reference in New Issue
Block a user