|
|
#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<void*> 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<POSITION> 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<Point> candidatePoints;
|
|
|
int pOneType = pOne->GetType();
|
|
|
|
|
|
if (pOneType == DOUBLEFOX_POINT)
|
|
|
{
|
|
|
CPointNameEx* pPoint = pOne->GetValueSafe<CPointNameEx>();
|
|
|
candidatePoints.push_back({ pPoint->x0, pPoint->y0 });
|
|
|
}
|
|
|
else if (pOneType == DOUBLEFOX_CURVE)
|
|
|
{
|
|
|
CCurveEx* pCurve = pOne->GetValueSafe<CCurveEx>();
|
|
|
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<double>::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<CCurveEx>();
|
|
|
|
|
|
SnapResult bestLocal = noneResult();
|
|
|
bestLocal.distanceSq = std::numeric_limits<double>::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<void*> m_ingoreSet; // 忽略列表,里面存储 COne Value 指针
|
|
|
};
|
|
|
|
|
|
}
|