#pragma once #include #include #include #include #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& 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& 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> BuildClusters(const std::vector& polygons) { std::vector> clusters; std::vector> 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 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& polygons, const std::vector>& 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& 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& 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")