#pragma once #include "SpatialIndex.h" #include "DrawOperator/Xy.h" #include "GeometryUtils.h" namespace Geometry { /** * \brief 吸附配置参数 * 用于控制吸附的灵敏度和判定范围 */ struct SnapConfig { double searchRadius = 0.0; // [粗筛] 空间索引搜索半径 (通常比 snapDistance 大一点,用于获取候选集) double angleTolerance = 0.0; // [角度] 平行判定容差,单位:度 (Degrees)。例如 5.0 表示 ±5 度以内视为平行 double snapDistance = 0.0; // [阈值] 最小吸附距离阈值 (真实坐标单位)。小于此距离才会触发吸附 }; /** * \brief 吸附类型枚举 */ enum class SnapType { NONE, // 未吸附 POINT, // 吸附到某个特定点 (端点、中点等) PARALLEL_LINE, // 吸附到某条平行线段 (实现共线) }; /** * \brief 吸附结果描述结构 * 包含吸附后的位置信息、偏移量以及被吸附的目标信息 */ struct SnapResult { SnapType snapType = SnapType::NONE; // 吸附类型 double distanceSq = 0.0; // 原始距离的平方(用于多候选时的优先级比对) POSITION sourceElement = nullptr; // 被吸附的目标图元 (Source Object) Point offset = { 0.0, 0.0 }; // [核心数据] 修正偏移向量。InputPoint + Offset = SnappedPoint Segment snappedSegment; // 吸附后的新线段 (预览用,已应用 offset) Point snappedReferencePoint; // 视觉参考点 (例如:吸附到了哪个端点,或者吸附到了目标线的哪个投影点) Segment targetSegment; // [平行吸附专用] 记录目标线段的几何信息,用于后续的投影计算 }; /** * \brief 线段吸附引擎 * 核心职责: * 给定一条输入线段 (Input Segment),在空间索引中寻找最近的、符合条件的几何特征 (点或平行线)。 * 计算出将输入线段“移动”到目标特征所需的最小偏移量。 */ class SnappingEngine { public: /** * \brief 构造函数 * \param pXy 图元容器指针 * \param index 空间索引 (QuadTree/R-Tree) * \param config 吸附参数配置 * \param ignoreSet 忽略列表 (通常包含当前正在被拖动的图元自身,防止吸附自己) */ SnappingEngine(CXy* pXy, const CSpatialIndex& index, const SnapConfig& config, std::set ignoreSet = {}) : m_pXy(pXy), m_index(index), m_config(config), m_ingoreSet(ignoreSet) { } /** * \brief 计算最佳吸附结果 * * 流程: * 1. AABB 查询:获取线段周围的候选图元。 * 2. 竞标机制:遍历所有候选图元,分别计算“点吸附”和“平行线吸附”。 * 3. 优胜劣汰:保留距离最近的点吸附结果和线吸附结果。 * 4. 最终决策:比较最佳点吸附和最佳线吸附,返回距离更近的那个。 * \param inputSegment 当前鼠标拖动产生的临时线段 * \return SnapResult 最佳吸附结果,如果没有合适目标则返回 SnapType::NONE */ SnapResult getSnapResult(const Segment& inputSegment) { // Step 1: 获取搜索区域 (线段 AABB 向外扩充吸附距离) CRect8 area = getLineSegmentAABB(inputSegment, m_config.snapDistance); // Step 2: 空间索引查询 std::vector positions = m_index.FindElementsInRect(area); SnapResult bestPointSnap; SnapResult bestLineSnap; // Step 3: 遍历候选集进行详细几何计算 for (POSITION pos : positions) { SnapResult pResult = calcPointSnap(inputSegment, pos); if (isBetter(pResult, bestPointSnap)) { bestPointSnap = pResult; } SnapResult lResultl = calcParallelSnap(inputSegment, pos); if (isBetter(lResultl, bestLineSnap)) { bestLineSnap = lResultl; } } // Step 4: 决出最终赢家 (Point vs Line) SnapResult finalResult = decideWinner(bestPointSnap, bestLineSnap); // Step 5: 阈值截断 (如果最近的距离依然超过了 snapDistance,则放弃 if (finalResult.distanceSq <= m_config.snapDistance) { return finalResult; } else { return noneResult(); } } private: CRect8 getLineSegmentAABB(const Segment& segment, double width) const { double halfWidth = width / 2.0; double rawMinX = min(segment.start.x, segment.end.x); double rawMaxX = max(segment.start.x, segment.end.x); double rawMinY = min(segment.start.y, segment.end.y); double rawMaxY = max(segment.start.y, segment.end.y); return CRect8(rawMinX - halfWidth, rawMaxY + halfWidth, rawMaxX + halfWidth, rawMinY - halfWidth); } /** * \brief 计算点吸附 (Point Snap) * * 逻辑: * 1. 提取目标图元的所有关键点 (端点、节点)。 * 2. 计算这些点到 inputSegment 所在直线的投影位置。 * 3. 约束检查:投影点必须落在 inputSegment 线段内部 (t ∈ [0, 1])。 * 原理:我们只希望线段的“侧面”去吸附别人的点,而不是无限延长线去吸附。 * 4. 记录最小距离。 */ SnapResult calcPointSnap(const Segment& inputSegment, POSITION target) const { COne* pOne = m_pXy->GetAt(target); if (!pOne) { return noneResult(); } if (inIngoreSet(pOne->value)) { return noneResult(); } // 1. 收集目标几何体所有的关键点 std::vector candidatePoints; int pOneType = pOne->GetType(); if (pOneType == DOUBLEFOX_POINT) { CPointNameEx* pPoint = pOne->GetValueSafe(); candidatePoints.push_back({ pPoint->x0, pPoint->y0 }); } else if (pOneType == DOUBLEFOX_CURVE) { CCurveEx* pCurve = pOne->GetValueSafe(); for (int i = 0; i < pCurve->num; i++) { candidatePoints.push_back({ pCurve->x[i], pCurve->y[i] }); } } SnapResult bestLocal = noneResult(); bestLocal.distanceSq = std::numeric_limits::max(); double snapDistSqLimit = m_config.snapDistance * m_config.snapDistance; // 2. 遍历所有候选点 for (const auto& targetPt : candidatePoints) { // 计算 目标点 在 输入线段 上的投影因子 t // 公式: P_proj = Start + t * Dir double t = GeometryUtils::getProjectionFactor(targetPt, inputSegment); // [核心约束]: 只有当 t 在 [0, 1] 之间时,点才位于线段的“正侧方” (法线区域) // 如果不加这个限制,线段的延长线也会吸附到远处的点,体验会很差 if (t >= 0.0 && t <= 1.0) { // 计算线段上的投影点坐标 Point dir = inputSegment.direction(); Point projPoint = inputSegment.start + (dir * t); // 计算偏移向量:从 投影点 指向 目标点 (这就是线段需要移动的向量) Point offset = targetPt - projPoint; double currentDistSq = offset.lengthSq(); // 3. 距离判定与更新 if (currentDistSq <= snapDistSqLimit && currentDistSq < bestLocal.distanceSq) { bestLocal.snapType = SnapType::POINT; bestLocal.sourceElement = target; bestLocal.distanceSq = currentDistSq; // 填充结果 bestLocal.offset = offset; bestLocal.snappedReferencePoint = targetPt; // 吸附到了这个点 // 生成吸附后的新线段 (原线段平移 offset) bestLocal.snappedSegment.start = inputSegment.start + offset; bestLocal.snappedSegment.end = inputSegment.end + offset; } } } return bestLocal; } /** * \brief 计算平行线吸附 (Parallel Line Snap) * * 逻辑: * 1. 遍历目标曲线的所有线段。 * 2. 平行检查:计算两向量夹角余弦值,判断是否在 angleTolerance 范围内。 * 3. 重叠检查:防止两条平行线虽然在同一直线上,但相距十万八千里(投影无交集)。 * 4. 距离计算:计算 inputSegment 起点到 targetSegment 所在直线的垂直距离。 */ SnapResult calcParallelSnap(const Segment& inputSegment, POSITION target) const { COne* pOne = m_pXy->GetAt(target); // 只处理曲线/线段类型 if (!pOne || pOne->GetType() != DOUBLEFOX_CURVE) { return noneResult(); } if (inIngoreSet(pOne->value)) { return noneResult(); } CCurveEx* pCurve = pOne->GetValueSafe(); SnapResult bestLocal = noneResult(); bestLocal.distanceSq = std::numeric_limits::max(); double snapDistSqLimit = m_config.snapDistance * m_config.snapDistance; // 遍历目标折线的所有线段 for (int i = 0; i < pCurve->num - 1; i++) { Segment targetSeg; targetSeg.start = { pCurve->x[i], pCurve->y[i] }; targetSeg.end = { pCurve->x[i + 1], pCurve->y[i + 1] }; // 1. 判定是否接近平行 (角度容差) if (GeometryUtils::isNearlyParallel(inputSegment, targetSeg, m_config.angleTolerance)) { // 2. 判定是否在投影方向上重叠 (防止吸附到无限远处的延长线) if (GeometryUtils::areSegmentsOverlapping(inputSegment, targetSeg)) { // 3. 计算垂直距离和偏移向量 // 方法:将 inputSegment 的起点投影到 targetSeg 所在的直线上 double t = GeometryUtils::getProjectionFactor(inputSegment.start, targetSeg); Point targetDir = targetSeg.direction(); Point projStart = targetSeg.start + (targetDir * t); // 偏移向量 = 投影点 - 输入线段起点 Point offset = projStart - inputSegment.start; double currentDistSq = offset.lengthSq(); if (currentDistSq <= snapDistSqLimit && currentDistSq < bestLocal.distanceSq) { bestLocal.snapType = SnapType::PARALLEL_LINE; bestLocal.sourceElement = target; bestLocal.distanceSq = currentDistSq; // 填充结果 bestLocal.offset = offset; bestLocal.snappedReferencePoint = projStart; // 视觉参考点 // 生成吸附后的新线段 bestLocal.snappedSegment.start = inputSegment.start + offset; bestLocal.snappedSegment.end = inputSegment.end + offset; bestLocal.targetSegment = targetSeg; } } } } return bestLocal; } bool isBetter(const SnapResult& current, const SnapResult& target) { if (target.snapType == SnapType::NONE) { return true; } if (current.snapType != SnapType::NONE && current.snapType == target.snapType) { return current.distanceSq < target.distanceSq; } return false; } SnapResult decideWinner(const SnapResult& current, const SnapResult& target) const { if (current.snapType != SnapType::NONE && target.snapType != SnapType::NONE) { return current.distanceSq < target.distanceSq ? current : target; } return current.snapType == SnapType::NONE ? target : current; } SnapResult noneResult() const { SnapResult result; result.sourceElement = nullptr; result.snapType = SnapType::NONE; return result; } double calcDistanceSq(double dx, double dy) const { return dx * dx + dy * dy; } bool inIngoreSet(void* pValue) const { return m_ingoreSet.find(pValue) != m_ingoreSet.end(); } const CSpatialIndex& m_index; SnapConfig m_config; CXy* m_pXy = nullptr; std::set m_ingoreSet; // 忽略列表,里面存储 COne Value 指针 }; }