本质上是约束条件下的路径规划问题,核心是找到一条连续路径遍历所有点,同时满足: 路径不与自身交叉; 路径全程在多段线(多边形)内部; 路径连续(一笔画)。
核心思路与算法步骤 这类问题没有 “万能算法”,但可以通过组合几何算法实现,以下是一套可行方案:
1. 基础几何工具(前置条件) 首先需要实现几个基础几何检测工具,用于后续约束验证: 点是否在多边形内:用射线法(从点向任意方向发射射线,统计与多边形边的交点数,奇数则在内部)。 线段是否与多边形边界交叉:检测线段是否超出多边形(除了端点可能在边界上)。 两线段是否交叉:判断两条线段是否存在交点(非端点重合)。
2. 核心算法:分层贪婪路径法 步骤如下:
(1)点集预处理 确保所有点都在多段线(多边形)内部(用步骤 1 的工具验证,剔除或修正外部点)。 计算多边形的 “内核”(若为凹多边形,可能存在不可见区域,需确保点在可见区域内)。 (2)构建非交叉路径的核心逻辑 采用 “凸包分层 + 极角排序” 的策略,从外到内逐层连接点,避免交叉: 提取当前点集的凸包: 凸包是包含所有点的最小凸多边形,凸包上的点按顺时针 / 逆时针排序后连接,天然无交叉。 连接凸包上的点: 按凸包顺序(如逆时针)连接凸包顶点,形成外层路径。 处理凸包内部的点: 剔除凸包上的点后,对剩余内部点重复步骤 1-2,形成内层路径,最后将内层路径与外层路径连接(选择一个连接点,确保连线不交叉且在多边形内)。 (3)边界约束验证 每生成一条线段(两点之间的连线),都需验证: 线段是否完全在多边形内部(无跨边界); 线段是否与已生成的路径交叉。
3. 算法优化(可选) 若点集规模大(>100),可引入三角剖分:将多边形内部分割为三角形网格,在网格内规划路径(三角形内的连线天然不交叉)。 结合最小生成树(MST):先通过 MST 生成无交叉的骨架,再按 MST 的边序连接点(需确保 MST 的边都在多边形内)。 算法逻辑:分层凸包连接法(改进版) 该算法通过 “外层优先、逐层向内” 的策略连接点,利用凸包的几何特性避免交叉,同时通过边界检测确保路径不出界。步骤如下: 初始筛选:确认所有点均在多段线内部(前置条件,避免后续重复检测)。
凸包分层: 对当前点集计算凸包(最外层点构成的凸多边形),凸包上的点按逆时针排序后连接,天然无交叉。 从点集中移除已连接的凸包点,对剩余点重复计算凸包,形成内层点集。 层间连接:每处理完一层凸包后,从当前层的终点到下一层的起点找一条合法连线(不交叉、在多边形内)。
最终路径:按 “外层→内层→更内层” 的顺序拼接所有层的路径,形成完整遍历路径。 关键约束验证 连接过程中需实时验证两条核心规则,确保路径合法性: 不出界:任意两点的连线必须完全在多段线内部(通过 “线段与多边形边界无交叉” 验证)。 不交叉:新生成的线段不得与已存在的路径线段相交(通过 “线段交叉检测” 验证)。
C# 实现代码 以下代码基于 AutoCAD 的Autodesk.AutoCAD库实现,包含路径生成的完整逻辑(依赖前文提到的几何工具函数):
using System;
using System.Collections.Generic;
using System.Linq;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.DatabaseServices;
public class PathGenerator
{
// 多段线边界(封闭多边形)
private Polyline _boundaryPolyline;
// 已生成的路径线段(用于交叉检测)
private List<Tuple<Point3d, Point3d>> _existingSegments = new List<Tuple<Point3d, Point3d>>();
public PathGenerator(Polyline boundary)
{
_boundaryPolyline = boundary;
}
/// <summary>
/// 连接所有点,生成无交叉、不出界的路径
/// </summary>
/// <param name="points">待连接的点集(已确认在多段线内部)</param>
/// <returns>路径点序列(按连接顺序排列)</returns>
public List<Point3d> ConnectPoints(List<Point3d> points)
{
if (points == null || points.Count == 0)
return new List<Point3d>();
// 复制点集避免修改原始数据
List<Point3d> remainingPoints = new List<Point3d>(points);
List<Point3d> fullPath = new List<Point3d>();
// 递归处理所有层的点
while (remainingPoints.Count > 0)
{
// 1. 计算当前点集的凸包(外层点)
List<Point3d> currentHull = GetConvexHull(remainingPoints);
if (currentHull.Count == 0)
break;
// 2. 按逆时针顺序连接凸包点(确保无交叉)
List<Point3d> hullPath = OrderHullPoints(currentHull);
// 3. 验证并添加凸包路径到总路径
if (fullPath.Count == 0)
{
// 首次添加:直接加入凸包路径
fullPath.AddRange(hullPath);
}
else
{
// 非首次添加:先连接上一层终点与当前层起点
Point3d lastPoint = fullPath.Last();
Point3d firstHullPoint = hullPath.First();
// 找到合法的连接点(可能需要调整起点)
var (validStart, validLast) = FindValidConnection(lastPoint, hullPath, remainingPoints);
if (validStart == null || validLast == null)
throw new Exception("无法找到合法的层间连接点");
// 添加连接线段
fullPath.Add(validStart.Value);
// 添加当前层路径(从连接点开始)
int startIndex = hullPath.IndexOf(validStart.Value);
fullPath.AddRange(hullPath.Skip(startIndex));
}
// 4. 从剩余点中移除当前凸包点
remainingPoints = remainingPoints.Except(currentHull).ToList();
}
return fullPath;
}
/// <summary>
/// 对凸包点按逆时针排序(确保连接无交叉)
/// </summary>
private List<Point3d> OrderHullPoints(List<Point3d> hull)
{
if (hull.Count <= 1)
return hull;
// 以凸包重心为基准,按极角排序
Point3d centroid = CalculateCentroid(hull);
return hull.OrderBy(p => GetPolarAngle(centroid, p)).ToList();
}
/// <summary>
/// 计算点集的重心(用于极角排序)
/// </summary>
private Point3d CalculateCentroid(List<Point3d> points)
{
double x = points.Average(p => p.X);
double y = points.Average(p => p.Y);
return new Point3d(x, y, 0); // 忽略Z轴
}
/// <summary>
/// 寻找上一层终点到当前凸包的合法连接点
/// </summary>
private (Point3d? start, Point3d? last) FindValidConnection(
Point3d lastPoint, List<Point3d> hullPath, List<Point3d> remainingPoints)
{
// 尝试当前凸包的每个点作为连接起点
foreach (var candidate in hullPath)
{
// 验证连线是否合法(不出界、不交叉)
if (IsSegmentValid(lastPoint, candidate))
{
return (candidate, lastPoint);
}
}
// 若直接连接失败,尝试通过最近点中转(简化处理)
var nearest = remainingPoints
.OrderBy(p => Distance(lastPoint, p))
.FirstOrDefault();
if (nearest != null && IsSegmentValid(lastPoint, nearest))
{
return (nearest, lastPoint);
}
return (null, null);
}
/// <summary>
/// 验证线段是否合法(不出界、不与已有路径交叉)
/// </summary>
private bool IsSegmentValid(Point3d a, Point3d b)
{
// 1. 验证线段是否完全在多段线内部
if (!IsSegmentInsidePolygon(a, b))
return false;
// 2. 验证线段是否与已有路径交叉
foreach (var seg in _existingSegments)
{
if (DoSegmentsIntersect(a, b, seg.Item1, seg.Item2))
return false;
}
// 3. 记录合法线段(用于后续检测)
_existingSegments.Add(Tuple.Create(a, b));
return true;
}
// ------------------------------
// 以下为依赖的几何工具函数(简化版)
// ------------------------------
/// <summary>
/// 计算凸包(Graham扫描法,参考前文)
/// </summary>
private List<Point3d> GetConvexHull(List<Point3d> points)
{
// 实现同前文,此处简化
if (points.Count <= 3)
return points;
// (实际实现需补充Graham扫描逻辑)
return points;
}
/// <summary>
/// 验证线段是否完全在多边形内部
/// </summary>
private bool IsSegmentInsidePolygon(Point3d a, Point3d b)
{
// 简化逻辑:线段两端点在内部,且线段不与多边形边界交叉
if (!IsPointInPolygon(a) || !IsPointInPolygon(b))
return false;
// (实际需补充线段与多边形边界的交叉检测)
return true;
}
/// <summary>
/// 判断点是否在多边形内(射线法,参考前文)
/// </summary>
private bool IsPointInPolygon(Point3d p)
{
// (实现同前文)
return true;
}
/// <summary>
/// 判断两线段是否交叉
/// </summary>
private bool DoSegmentsIntersect(Point3d a1, Point3d a2, Point3d b1, Point3d b2)
{
// (实现线段交叉检测,基于叉积判断方向)
return false;
}
/// <summary>
/// 计算极角(用于排序)
/// </summary>
private double GetPolarAngle(Point3d pivot, Point3d p)
{
return Math.Atan2(p.Y - pivot.Y, p.X - pivot.X);
}
/// <summary>
/// 计算两点距离
/// </summary>
private double Distance(Point3d a, Point3d b)
{
return Math.Sqrt(Math.Pow(a.X - b.X, 2) + Math.Pow(a.Y - b.Y, 2));
}
}