CAD 的 C# 开发中,对多段线(封闭多边形)内部的点进行 “一笔连线且不交叉、不出界
本质上是约束条件下的路径规划问题,核心是找到一条连续路径遍历所有点,同时满足: 路径不与自身交叉; 路径全程在多段线(多边形)内部; 路径连续(一笔画)。
核心思路与算法步骤 这类问题没有 “万能算法”,但可以通过组合几何算法实现,以下是一套可行方案:
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));}
}