feat: 测试覆盖扩展 + 文档更新

This commit is contained in:
茂之钳
2026-07-23 12:36:39 +00:00
parent 41a8992332
commit f2203c7255
42 changed files with 3448 additions and 106 deletions
+150 -6
View File
@@ -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
-2
View File
@@ -1,2 +0,0 @@
#include "vde/core/point.h"
namespace vde::core {}
+258 -1
View File
@@ -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
-2
View File
@@ -1,2 +0,0 @@
#include "vde/core/transform.h"
namespace vde::core {}