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.
kev/Drawer/Module/GeoSigmaDraw/SegmentSnappingEngine.h

339 lines
9.6 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 "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 指针
};
}