|
|
#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") |