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.

496 lines
15 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 <vector>
#include <cmath>
#include <algorithm>
#include <map>
#include "DrawOperator/CurveEx.h" // 确保包含 CurveEx 的定义
#pragma push_macro("min")
#pragma push_macro("max")
#undef min
#undef max
/**
* \struct Vec2
* \brief 用于几何计算的简单二维向量结构。
*/
struct Vec2
{
double x; ///< X 坐标
double y; ///< Y 坐标
/**
* \brief 向量加法。
*/
Vec2 operator+(const Vec2& v) const
{
return { x + v.x, y + v.y };
}
/**
* \brief 向量减法。
*/
Vec2 operator-(const Vec2& v) const
{
return { x - v.x, y - v.y };
}
/**
* \brief 标量乘法。
*/
Vec2 operator*(double s) const
{
return { x * s, y * s };
}
/**
* \brief 计算两个向量的点积。
*/
double dot(const Vec2& v) const
{
return x * v.x + y * v.y;
}
/**
* \brief 计算向量长度的平方。
* \note 比 length() 快,因为它避免了开方运算。
*/
double lengthSq() const
{
return x * x + y * y;
}
/**
* \brief 计算向量的长度 (模)。
*/
double length() const
{
return std::sqrt(lengthSq());
}
};
/**
* \class RigidSnapper
* \brief 职责类 1刚性平移器。
* \details 该类负责计算并应用整体平移,将多边形吸附到附近的目标。
* 它严格保持多边形的原始形状(不发生形变),仅改变位置。
*/
class RigidSnapper
{
public:
/**
* \brief 应用刚性平移以对齐多边形。
* \param polygons 需要处理的多边形列表。
* \param threshold 吸附的最大距离阈值。
* \param angleDeg 判定边平行的最大角度差(度)。
*/
static void Apply(std::vector<CCurveEx*>& polygons, double threshold, double angleDeg)
{
if (polygons.size() < 2)
{
return;
}
double angleRad = angleDeg * 3.1415926535 / 180.0;
double minCos = std::cos(angleRad);
for (size_t i = 0; i < polygons.size(); ++i)
{
CCurveEx* sourcePolygon = polygons[i];
Vec2 bestTranslation = { 0, 0 };
double minDistance = threshold;
bool foundSnap = false;
for (size_t j = 0; j < polygons.size(); ++j)
{
if (i == j)
{
continue;
}
CCurveEx* targetPolygon = polygons[j];
// 稳定性检查:只向比自己大的物体或尺寸相近的物体吸附。
// 这可以防止小碎屑拖动大的结构体。
if (GetArea(targetPolygon) < GetArea(sourcePolygon) * 0.8)
{
continue;
}
Vec2 edgeMove;
double edgeDistance = 0.0;
if (FindBestEdgeSnap(sourcePolygon, targetPolygon, minCos, edgeMove, edgeDistance))
{
if (edgeDistance < minDistance)
{
minDistance = edgeDistance;
bestTranslation = edgeMove;
foundSnap = true;
}
}
}
// 如果找到了有效的吸附目标,应用平移
if (foundSnap)
{
for (int k = 0; k < sourcePolygon->num; ++k)
{
sourcePolygon->x[k] += bestTranslation.x;
sourcePolygon->y[k] += bestTranslation.y;
}
}
}
}
private:
/**
* \brief 计算多边形的面积。
*/
static double GetArea(CCurveEx* polygon)
{
return polygon->Area();
}
/**
* \brief 检查两条线段在目标线轴上的投影是否重叠。
* \details 这可以防止吸附那些虽然平行且距离近,但在纵向上完全错开的线段。
*/
static bool IsLineOverlap(Vec2 a1, Vec2 a2, Vec2 b1, Vec2 b2)
{
Vec2 axis = b2 - b1;
if (axis.lengthSq() < 1e-9)
{
return false;
}
auto project = [&](Vec2 p) { return p.dot(axis); };
double minA = std::min(project(a1), project(a2));
double maxA = std::max(project(a1), project(a2));
double minB = std::min(project(b1), project(b2));
double maxB = std::max(project(b1), project(b2));
// 检查区间重叠
return std::max(minA, minB) < std::min(maxA, maxB) - 1e-3;
}
/**
* \brief 寻找将源多边形的边对齐到目标多边形边的最佳平移向量。
* \return 如果找到合适的边吸附则返回 true。
*/
static bool FindBestEdgeSnap(CCurveEx* sourcePolygon, CCurveEx* targetPolygon, double minCos, Vec2& outMove, double& outDistance)
{
bool found = false;
outDistance = 1e9;
for (int i = 0; i < sourcePolygon->num - 1; ++i)
{
Vec2 sourcePoint1 = { sourcePolygon->x[i], sourcePolygon->y[i] };
Vec2 sourcePoint2 = { sourcePolygon->x[i + 1], sourcePolygon->y[i + 1] };
Vec2 sourceDirection = sourcePoint2 - sourcePoint1;
if (sourceDirection.lengthSq() < 1e-9)
{
continue;
}
Vec2 sourceNormal = { sourceDirection.y, -sourceDirection.x };
sourceNormal = sourceNormal * (1.0 / sourceDirection.length());
for (int j = 0; j < targetPolygon->num - 1; ++j)
{
Vec2 targetPoint1 = { targetPolygon->x[j], targetPolygon->y[j] };
Vec2 targetPoint2 = { targetPolygon->x[j + 1], targetPolygon->y[j + 1] };
Vec2 targetDirection = targetPoint2 - targetPoint1;
if (targetDirection.lengthSq() < 1e-9)
{
continue;
}
Vec2 targetNormal = { targetDirection.y, -targetDirection.x };
targetNormal = targetNormal * (1.0 / targetDirection.length());
// 检查 1: 线段是否平行?
if (std::abs(sourceNormal.dot(targetNormal)) < minCos)
{
continue;
}
// 检查 2: 投影是否重叠?
if (!IsLineOverlap(sourcePoint1, sourcePoint2, targetPoint1, targetPoint2))
{
continue;
}
// 计算源线段中心到目标直线的垂直距离
Vec2 sourceMidPoint = (sourcePoint1 + sourcePoint2) * 0.5;
double distance = std::abs((sourceMidPoint - targetPoint1).dot(targetNormal));
if (distance < outDistance)
{
outMove = targetNormal * (-(sourceMidPoint - targetPoint1).dot(targetNormal));
outDistance = distance;
found = true;
}
}
}
return found;
}
};
/**
* \class ClusterGapFiller
* \brief 职责类 2聚类补缝器。
* \details 该类负责处理微小变形和缝隙修复。
* 它识别位置重合的顶点簇(以确保联动移动),并将它们投影到最近的边上以闭合缝隙。
* 支持吸附到直线的延伸线上以修复端点问题。
*/
class ClusterGapFiller
{
public:
/**
* \brief 应用补缝逻辑。
* \param polygons 要修改的多边形列表。
* \param snapRadius 寻找目标线的搜索半径。
* \param maxExtension 允许点沿着直线延伸方向吸附的最大距离。
*/
static void Apply(std::vector<CCurveEx*>& polygons, double snapRadius, double maxExtension)
{
if (polygons.empty())
{
return;
}
// 1. 聚类阶段 (Clustering Phase)
// 找出所有物理上位置重合的顶点,并将它们编组为“簇”。
// 这样保证了如果多边形 A 和多边形 C 共用一个顶点,它们会一起移动。
auto clusters = BuildClusters(polygons);
// 2. 吸附阶段 (Snapping Phase)
// 将每个簇作为一个整体处理,吸附到最佳的目标线上。
ApplySnapToClusters(polygons, clusters, snapRadius, maxExtension);
// 3. 清理阶段 (Cleanup Phase)
// 确保闭合多边形保持闭合(首尾点一致)。
EnsureClosure(polygons);
}
private:
/**
* \struct VertexReference
* \brief 指向多边形列表中特定顶点的句柄。
*/
struct VertexReference
{
int polygonIndex;
int vertexIndex;
};
/**
* \brief 构建顶点簇列表。
* \details 簇是指来自不同多边形但共享相同(或极近)坐标的一组顶点。
*/
static std::vector<std::vector<VertexReference>> BuildClusters(const std::vector<CCurveEx*>& polygons)
{
std::vector<std::vector<VertexReference>> clusters;
std::vector<std::vector<bool>> visited(polygons.size());
for (size_t i = 0; i < polygons.size(); ++i)
{
visited[i].resize(polygons[i]->num, false);
}
double weldToleranceSq = 1e-4 * 1e-4; // 判定两点“重合”的误差平方
for (size_t i = 0; i < polygons.size(); ++i)
{
for (int k = 0; k < polygons[i]->num; ++k)
{
if (visited[i][k])
{
continue;
}
std::vector<VertexReference> cluster;
Vec2 vertex1 = { polygons[i]->x[k], polygons[i]->y[k] };
// 将自己加入新簇
cluster.push_back({ (int)i, k });
visited[i][k] = true;
// 在其他多边形中寻找“兄弟”
for (size_t j = 0; j < polygons.size(); ++j)
{
for (int m = 0; m < polygons[j]->num; ++m)
{
if (i == j && k == m) continue;
if (visited[j][m]) continue;
Vec2 vertex2 = { polygons[j]->x[m], polygons[j]->y[m] };
if ((vertex1 - vertex2).lengthSq() < weldToleranceSq)
{
cluster.push_back({ (int)j, m });
visited[j][m] = true;
}
}
}
clusters.push_back(cluster);
}
}
return clusters;
}
/**
* \brief 为每个簇寻找最近的线,并移动簇中的所有顶点。
*/
static void ApplySnapToClusters(std::vector<CCurveEx*>& polygons,
const std::vector<std::vector<VertexReference>>& clusters,
double snapRadius,
double maxExtension)
{
double snapRadiusSq = snapRadius * snapRadius;
for (const auto& cluster : clusters)
{
// 使用簇中的第一个顶点作为代表位置
const auto& representative = cluster[0];
Vec2 point = { polygons[representative.polygonIndex]->x[representative.vertexIndex],
polygons[representative.polygonIndex]->y[representative.vertexIndex] };
Vec2 bestPosition = point;
double minDistanceSquared = snapRadiusSq;
bool found = false;
// 遍历寻找所有潜在的目标线
for (size_t j = 0; j < polygons.size(); ++j)
{
CCurveEx* targetPolygon = polygons[j];
if (targetPolygon->num < 2)
{
continue;
}
// 过滤:不要吸附到属于该簇一部分的多边形上。
// 这防止了自吸附或图形塌陷。
bool isSelf = false;
for (const auto& member : cluster)
{
if (member.polygonIndex == (int)j)
{
isSelf = true;
break;
}
}
if (isSelf)
{
continue;
}
for (int m = 0; m < targetPolygon->num - 1; ++m)
{
Vec2 linePointA = { targetPolygon->x[m], targetPolygon->y[m] };
Vec2 linePointB = { targetPolygon->x[m + 1], targetPolygon->y[m + 1] };
Vec2 segmentVector = linePointB - linePointA;
Vec2 pointToStartVector = point - linePointA;
double lengthSquared = segmentVector.lengthSq();
if (lengthSquared < 1e-9)
{
continue;
}
// 计算投影系数 t
double t = pointToStartVector.dot(segmentVector) / lengthSquared;
// 健壮逻辑允许吸附到直线延伸线上maxExtension 范围内)。
// 这修复了点稍微超出线段端点的缝隙问题。
double segmentLength = std::sqrt(lengthSquared);
double distanceAlongLine = t * segmentLength;
if (distanceAlongLine < -maxExtension || distanceAlongLine > segmentLength + maxExtension)
{
continue;
}
// 计算投影点坐标
Vec2 projectionPoint = linePointA + segmentVector * t;
double currentDistanceSquared = (point - projectionPoint).lengthSq();
if (currentDistanceSquared < minDistanceSquared)
{
minDistanceSquared = currentDistanceSquared;
bestPosition = projectionPoint;
found = true;
}
}
}
// 同时将移动应用到簇中的所有成员
if (found)
{
for (const auto& member : cluster)
{
polygons[member.polygonIndex]->x[member.vertexIndex] = bestPosition.x;
polygons[member.polygonIndex]->y[member.vertexIndex] = bestPosition.y;
}
}
}
}
/**
* \brief 确保闭合多边形保持闭合状态。
*/
static void EnsureClosure(std::vector<CCurveEx*>& polygons)
{
for (auto* polygon : polygons)
{
if (polygon->num > 2)
{
polygon->x[polygon->num - 1] = polygon->x[0];
polygon->y[polygon->num - 1] = polygon->y[0];
}
}
}
};
/**
* \class PolygonSnapper
* \brief 多边形吸附系统的外观模式类 (Facade)。
* \details 协调两阶段的吸附过程:
* 1. 刚性平移 (Rigid Translation):整体对齐,不改变形状。
* 2. 聚类补缝 (Cluster Gap Filling):局部微调顶点以闭合缝隙。
*/
class PolygonSnapper
{
public:
/**
* \brief 执行吸附的主入口。
* \param polygons 需要处理的多边形列表。
* \param rigidThreshold 阶段 1 的阈值(刚性平移)。
* \param angleThresholdDeg 阶段 1 的角度容差(度)。
* \param snapRadius 阶段 2 的搜索半径(补缝)。
* \param maxExtension 阶段 2 允许沿直线延伸吸附的最大长度(默认 100.0)。
*/
static void Snap(std::vector<CCurveEx*>& polygons,
double rigidThreshold,
double angleThresholdDeg,
double snapRadius,
double maxExtension = 100.0)
{
// 1. 刚性平移阶段
// 移动整个多边形以与目标对齐,保留原始形状。
RigidSnapper::Apply(polygons, rigidThreshold, angleThresholdDeg);
// 2. 补缝阶段
// 微调顶点(按簇分组)以闭合微小缝隙。
ClusterGapFiller::Apply(polygons, snapRadius, maxExtension);
}
};
#pragma pop_macro("min")
#pragma pop_macro("max")