You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

207 lines
4.5 KiB
C++

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#pragma once
#include <cmath>
#include <algorithm>
#include <vector>
namespace Geometry
{
struct Point
{
double x = 0.0;
double y = 0.0;
// 运算符重载,方便向量运算
Point operator+(const Point& other) const
{
return { x + other.x, y + other.y };
}
Point operator-(const Point& other) const
{
return { x - other.x, y - other.y };
}
Point operator*(double scalar) const
{
return { x * scalar, y * scalar };
}
// 向量点积
double dot(const Point& other) const
{
return x * other.x + y * other.y;
}
// 向量模长平方 (避免开方,用于性能优化)
double lengthSq() const
{
return x * x + y * y;
}
// 向量模长
double length() const
{
return std::sqrt(lengthSq());
}
};
struct Segment
{
Point start;
Point end;
// 获取线段的方向向量 (End - Start)
Point direction() const
{
return end - start;
}
};
// 结果结构体
struct PointSnapResult
{
Point closestPoint; // 线段上离目标最近的点
double distance; // 实际距离
};
/**
*
* GeometryUtils 工具类
*/
class GeometryUtils
{
public:
/**
* 计算点在直线上(由线段定义)的投影比例 t
* P_proj = Start + t * (End - Start)
*/
static double getProjectionFactor(const Point& point, const Segment& lineSeg)
{
Point ab = lineSeg.direction();
double lenSq = ab.lengthSq();
if (lenSq < std::numeric_limits<double>::epsilon())
{
return 0.0;
}
Point ap = point - lineSeg.start;
return ap.dot(ab) / lenSq;
}
/**
* 计算点到线段的最短距离及最近点
* \param point 查询点
* \param segment 目标线段
* \return 包含最近点坐标和距离的结构体
*/
static PointSnapResult pointToSegmentDistance(const Point& point, const Segment& segment)
{
Point ab = segment.direction();
Point ap = point - segment.start;
double abLenSq = ab.lengthSq();
// 边界情况线段长度为0退化为一个点
if (abLenSq < std::numeric_limits<double>::epsilon())
{
double dist = ap.length();
return { segment.start, dist };
}
// 计算投影系数 t
// t = (AP . AB) / |AB|^2
double t = ap.dot(ab) / abLenSq;
// 限制 t 在 [0, 1] 之间(夹具 Clamping
// 如果 t < 0说明最近点在 start 外侧;如果 t > 1在 end 外侧
t = max(0.0, min(1.0, t));
// 计算最近点坐标: Closest = Start + t * AB
Point closest = segment.start + (ab * t);
// 计算距离
double dist = (point - closest).length();
return { closest, dist };
}
/**
* 计算点到无限长直线的垂直距离平方
*/
static double pointToLineDistanceSq(const Point& point, const Segment& lineSeg)
{
Point ab = lineSeg.direction();
Point ap = point - lineSeg.start;
double abLenSq = ab.lengthSq();
if (abLenSq < std::numeric_limits<double>::epsilon())
{
return ap.lengthSq();
}
// 叉积的模 (2D叉积 = x1*y2 - x2*y1) 表示平行四边形面积
// 面积 = 底 * 高 => 高 = 面积 / 底
double crossProduct = ab.x * ap.y - ab.y * ap.x;
return (crossProduct * crossProduct) / abLenSq;
}
/**
* 检查两个共线/平行线段是否在投影方向上重叠
* 用于防止吸附到虽然平行但实际上错开很远的线段
*/
static bool areSegmentsOverlapping(const Segment& seg1, const Segment& seg2)
{
// 将 Seg2 的两个端点投影到 Seg1 的直线上,看 t 值范围
double t1 = getProjectionFactor(seg2.start, seg1);
double t2 = getProjectionFactor(seg2.end, seg1);
double minT = min(t1, t2);
double maxT = max(t1, t2);
// Seg1 的 t 范围是 [0, 1]
// 检查区间 [minT, maxT] 和 [0, 1] 是否有交集
return max(0.0, minT) <= min(1.0, maxT);
}
/**
* 判断两条线段是否“接近平行”
* \param seg1 线段1
* \param seg2 线段2
* \param toleranceDegrees 允许的偏差角度(度数)
* \return true 如果接近平行
*/
static bool isNearlyParallel(const Segment& seg1, const Segment& seg2, double toleranceDegrees)
{
Point v1 = seg1.direction();
Point v2 = seg2.direction();
double len1 = v1.length();
double len2 = v2.length();
// 防止零向量
if (len1 < std::numeric_limits<double>::epsilon() || len2 < std::numeric_limits<double>::epsilon())
{
return false;
}
// 计算单位向量的点积
// cos(theta) = (v1 . v2) / (|v1| * |v2|)
double cosTheta = v1.dot(v2) / (len1 * len2);
// 平行意味着角度接近 0度 (cos=1) 或 180度 (cos=-1)
// 所以我们需要检查 abs(cosTheta) 是否接近 1
// 将容差角度转换为弧度
double toleranceRad = toleranceDegrees * (M_PI / 180.0);
// 计算阈值。如果容差是 5度那么 cos(5度) 约为 0.996
// 只要 abs(cosTheta) > cos(tolerance),说明夹角小于 tolerance
double threshold = std::cos(toleranceRad);
return std::abs(cosTheta) >= threshold;
}
};
}