using System; using System.Collections.Generic; using System.Runtime.InteropServices; using ESRI.ArcGIS.Geometry; using GeoAPI.Operation.Buffer; using KGIS.Framework.AE; using NetTopologySuite.IO; using NetTopologySuite.Operation.Buffer; namespace Kingo.PluginServiceInterface.Helper { /// /// 狭长图形检测工具类(静态类) /// 功能:检测多边形中是否存在狭长区域(如长条状、细带状图斑) /// 核心算法思路:通过分析多边形环的顶点分布,识别局部密集点对,构建子多边形并通过紧凑度指标判断是否为狭长区域 /// public static class PolygonNarrowAreaChecker { /// /// 检测多边形中的局部狭长区域 /// /// 待检测的输入多边形(支持带内环的复杂多边形) /// 距离阈值(单位:与坐标系一致,默认1):用于判断两个顶点是否"过近",作为狭长区域的初步筛选条件 /// 检测到的狭长区域多边形列表(每个元素为一个狭长子多边形) public static List CheckLocalNarrowAreas(IPolygon polygon, double distanceThreshold = 1) { List narrowAreas = new List(); // 存储检测到的狭长区域 //var xxx = GeometryConvertHelper.ConvertIGeoemtryToWKT(polygon); string sNarrow = ISNarrow(polygon); if (!string.IsNullOrEmpty(sNarrow)) { #region 删除图形中的重复点(首位点除外) IGeometryCollection geomColl = polygon as IGeometryCollection; // 遍历所有子环(外环和内环) for (int i = 0; i < geomColl.GeometryCount; i++) { IGeometry subGeom = geomColl.get_Geometry(i); IRing ring = subGeom as IRing; // 环是闭合的线(IRing 继承自 IPolyline) if (ring == null) continue; // 处理当前环的重复点 RemoveDuplicatePointsInRing(ring); } #endregion IGeometryCollection geometryCollection = polygon as IGeometryCollection;// 将多边形转换为几何集合(用于遍历内部环) if (geometryCollection == null) return narrowAreas; // 非几何集合类型直接返回空列表 for (int ringIndex = 0; ringIndex < geometryCollection.GeometryCount; ringIndex++)// 遍历多边形的所有环(外环+内环) { IRing ring = geometryCollection.get_Geometry(ringIndex) as IRing; // 获取当前环 if (ring == null) continue; // 非环类型跳过 IPointCollection points = ring as IPointCollection;// 将环转换为点集合(用于获取顶点坐标) int pointCount = points.PointCount; // 环的总顶点数 bool isClosed = ring.IsClosed; // 判断环是否闭合(多边形环通常需闭合) int effectivePointCount = isClosed ? pointCount - 1 : pointCount; // 有效顶点数(闭合环需排除最后一个重复点) // 【步骤1】预处理环的顶点坐标:转换为结构体数组(提升计算性能)和IPoint对象数组(用于几何构建) var (ringPoints, ringPointsIPoint) = PreprocessRingPoints(points, effectivePointCount); if (ringPoints == null) continue; // 预处理失败时跳过当前环 HashSet<(double X, double Y)> usedPoints = new HashSet<(double X, double Y)>(); // 预计算所有线段的AABB(索引k对应线段k→k+1) var segmentAABBs = new (double MinX, double MaxX, double MinY, double MaxY)[effectivePointCount - 1]; for (int idx = 0; idx < effectivePointCount - 1; idx++) { IPoint pStart = ringPointsIPoint[idx]; IPoint pEnd = ringPointsIPoint[idx + 1]; segmentAABBs[idx] = ( MinX: Math.Min(pStart.X, pEnd.X), MaxX: Math.Max(pStart.X, pEnd.X), MinY: Math.Min(pStart.Y, pEnd.Y), MaxY: Math.Max(pStart.Y, pEnd.Y) ); } // 遍历所有可能的线段对(需优化遍历范围,避免O(n²)复杂度) for (int k = 0; k < effectivePointCount - 1; k++) { IPoint p1 = ringPointsIPoint[k]; IPoint p2 = ringPointsIPoint[k + 1]; // 检查p1/p2是否已被使用(通过坐标判断) if (usedPoints.Contains((p1.X, p1.Y)) || usedPoints.Contains((p2.X, p2.Y))) continue; // 获取当前线段k的AABB var aabbK = segmentAABBs[k]; for (int m = k + 2; m < effectivePointCount - 1; m++) // 跳过相邻线段 { IPoint p3 = ringPointsIPoint[m]; IPoint p4 = ringPointsIPoint[m + 1]; // 检查p3/p4是否已被使用 if (usedPoints.Contains((p3.X, p3.Y)) || usedPoints.Contains((p4.X, p4.Y))) continue; // 获取对比线段m的AABB var aabbM = segmentAABBs[m]; // AABB预筛选:判断两线段的包围盒是否可能相交 // 条件:x轴投影重叠且y轴投影重叠 double expandDistance = 1;//相当于外扩 bool xOverlap = (aabbK.MaxX + expandDistance) >= (aabbM.MinX - expandDistance) && (aabbK.MinX - expandDistance) <= (aabbM.MaxX + expandDistance); bool yOverlap = (aabbK.MaxY + expandDistance) >= (aabbM.MinY - expandDistance) && (aabbK.MinY - expandDistance) <= (aabbM.MaxY + expandDistance); if (!xOverlap || !yOverlap) continue; // AABB不重叠,直接跳过后续复杂计算 // 判断线段是否共点(如p1=p3, p2=p4等) if (ArePointsEqual(p1, p3) || ArePointsEqual(p1, p4) || ArePointsEqual(p2, p3) || ArePointsEqual(p2, p4)) continue; // 计算线段(p1,p2)和(p3,p4)的距离(仅保留AABB重叠的情况) Vector2D vecAB = CalculateVector(p1, p2); Vector2D vecCD = CalculateVector(p3, p4); bool isSameDirection = AreVectorsSameDirection(vecAB, vecCD, 0.01); if (isSameDirection) continue; var vangle = CalculateAngle(vecAB, vecCD); if (vangle < 25 || vangle > 155) continue; double distance = CalculateSegmentDistance(p1, p2, p3, p4); // var xxx1 = $@" //{GeometryConvertHelper.ConvertIGeoemtryToWKT(p1)} //{GeometryConvertHelper.ConvertIGeoemtryToWKT(p2)} //{GeometryConvertHelper.ConvertIGeoemtryToWKT(p3)} //{GeometryConvertHelper.ConvertIGeoemtryToWKT(p4)}"; if (distance < 0.1) { IPoint iIntersection = GetExtendedLineIntersection(p1, p2, p3, p4); //两条线段的交点 if (iIntersection != null) { double p1distance = PointToSegmentDistance(p1, p3, p4); double p2distance = PointToSegmentDistance(p2, p3, p4); double p3distance = PointToSegmentDistance(p3, p1, p2); double p4distance = PointToSegmentDistance(p4, p1, p2); int pindex_k = p1distance > p2distance ? k + 1 : k; int pindex_m = p3distance > p4distance ? m + 1 : m; double iIntersection12 = PointToSegmentDistance(iIntersection, p1, p2); double iIntersection34 = PointToSegmentDistance(iIntersection, p3, p4); IPoint point = null; if (iIntersection34 < iIntersection12) { point = p1distance > p2distance ? p2 : p1; } else { point = p3distance > p4distance ? p4 : p3; } try { IPolyline polyline1 = new PolylineClass(); polyline1.FromPoint = iIntersection; polyline1.ToPoint = point; polyline1.SpatialReference = polygon.SpatialReference; if (polyline1.Length < 0.1) { polyline1 = getExtendLine(polyline1, 3, 0.01); } var CutGeometryresult = FeatureAPI.CutGeometry(polygon, polyline1); Marshal.ReleaseComObject(polyline1); if (CutGeometryresult != null && CutGeometryresult.Count == 2) { foreach (var item in CutGeometryresult) { sNarrow = ISNarrow(item); if (sNarrow == "POLYGON EMPTY") { List pointList = null; var angle = SharpAngle.GetMinAngle1(item, ref pointList, false); if (angle < 10) continue; if (isThreeSides(item)) continue; narrowAreas.Add(item as IPolygon); usedPoints.Add((p1.X, p1.Y)); usedPoints.Add((p2.X, p2.Y)); usedPoints.Add((p3.X, p3.Y)); usedPoints.Add((p4.X, p4.Y)); } else if (!string.IsNullOrEmpty(sNarrow)) { narrowAreas = CheckLocalNarrowAreas(item as IPolygon); } } } else if (CutGeometryresult != null && CutGeometryresult.Count == 1) { continue; //IGeometry geo = CutGeometryresult[0]; //if (geo != null && !geo.IsEmpty) //{ // sNarrow = ISNarrow(geo); // if (!string.IsNullOrEmpty(sNarrow)) // { // List pointList = null; // var angle = SharpAngle.GetMinAngle1(geo, ref pointList, false); // if (angle < 10) continue; // narrowAreas.Add(geo as IPolygon); // usedPoints.Add((p1.X, p1.Y)); // usedPoints.Add((p2.X, p2.Y)); // usedPoints.Add((p3.X, p3.Y)); // usedPoints.Add((p4.X, p4.Y)); // continue; // } //} //else continue; } } catch (Exception ex) { continue; } } } } } } return narrowAreas; } return new List(); } private static IPoint GetExtendedLineIntersection(IPoint start1, IPoint end1, IPoint start2, IPoint end2) { // 创建原始线段 IPolyline polyline1 = new PolylineClass(); polyline1.FromPoint = start1; polyline1.ToPoint = end1; // 延长线段(假设 getExtendLine 可能返回包含中间点的多段线) IPolyline extendedPolyline1 = getExtendLine(polyline1, 3, 1); // 提取延长后的起点和终点,重建为仅含两点的线段 IPolyline simplifiedLine1 = new PolylineClass(); simplifiedLine1.FromPoint = extendedPolyline1.FromPoint; // 延长后的起点 simplifiedLine1.ToPoint = extendedPolyline1.ToPoint; // 延长后的终点 // 对第二条线段执行相同操作 IPolyline polyline2 = new PolylineClass(); polyline2.FromPoint = start2; polyline2.ToPoint = end2; IPolyline extendedPolyline2 = getExtendLine(polyline2, 3, 1); IPolyline simplifiedLine2 = new PolylineClass(); simplifiedLine2.FromPoint = extendedPolyline2.FromPoint; simplifiedLine2.ToPoint = extendedPolyline2.ToPoint; // 计算简化后线段的交点 ITopologicalOperator topoOp = simplifiedLine1 as ITopologicalOperator; IGeometry intersection = topoOp.Intersect(simplifiedLine2, esriGeometryDimension.esriGeometry0Dimension); // 处理交点结果(与原逻辑一致) if (intersection != null && intersection.GeometryType == esriGeometryType.esriGeometryPoint) { return intersection as IPoint; } else if (intersection.GeometryType == esriGeometryType.esriGeometryMultipoint) { IPointCollection points = intersection as IPointCollection; if (points.PointCount > 0) return points.get_Point(0); } return null; } /// /// 延长线段 /// /// 传入去的线 /// 模式,1为从FromPoint处延长,2为从ToPint处延长,3为两端延长 /// 延长的距离 /// /// 创建人:懒羊羊 public static IPolyline getExtendLine(IPolyline passLine, int mode, double dis) { IPointCollection pPointCol = passLine as IPointCollection; switch (mode) { case 1: IPoint fromPoint = new PointClass(); passLine.QueryPoint(esriSegmentExtension.esriExtendAtFrom, -1 * dis, false, fromPoint); pPointCol.InsertPoints(0, 1, ref fromPoint); break; case 2: IPoint endPoint = new PointClass(); object missing = Type.Missing; passLine.QueryPoint(esriSegmentExtension.esriExtendAtTo, dis + passLine.Length, false, endPoint); pPointCol.AddPoint(endPoint, ref missing, ref missing); break; case 3: IPoint fPoint = new PointClass(); IPoint ePoint = new PointClass(); object missing2 = Type.Missing; passLine.QueryPoint(esriSegmentExtension.esriExtendAtFrom, -1 * dis, false, fPoint); pPointCol.InsertPoints(0, 1, ref fPoint); passLine.QueryPoint(esriSegmentExtension.esriExtendAtTo, dis + passLine.Length, false, ePoint); pPointCol.AddPoint(ePoint, ref missing2, ref missing2); break; default: return pPointCol as IPolyline; } return pPointCol as IPolyline; } /// /// 判断图形是否为三角形 /// /// /// private static bool isThreeSides(IGeometry geometry) { bool isThreeSides = false; try { if (geometry is IPolygon polygon1) { IPointCollection pointCollection = polygon1 as IPointCollection; int vertexCount = pointCollection.PointCount; // 排除闭合多边形中重复的首尾顶点 IPoint startingpoint = pointCollection.get_Point(0);//起点 IPoint terminalpoint = pointCollection.get_Point(vertexCount - 1);//终点 if (polygon1.IsClosed && vertexCount > 1 && startingpoint.X == terminalpoint.X && startingpoint.Y == terminalpoint.Y) { vertexCount--; } isThreeSides = vertexCount == 3; } } catch (Exception ex) { } return isThreeSides; } /// /// 判断两个点是否共点(考虑浮点精度误差) /// /// 点1 /// 点2 /// 容差(默认1e-6,与坐标系单位一致) /// true:共点;false:不共点 private static bool ArePointsEqual(IPoint p1, IPoint p2, double tolerance = 1e-6) { return Math.Abs(p1.X - p2.X) < tolerance && Math.Abs(p1.Y - p2.Y) < tolerance; } /// /// 计算两条线段的最短距离(支持不相交、相交等场景) /// /// 线段1的起点 /// 线段1的终点 /// 线段2的起点 /// 线段2的终点 /// 两条线段的最短距离 private static double CalculateSegmentDistance(IPoint a1, IPoint a2, IPoint b1, IPoint b2) { // 向量定义:u为a线段方向,v为b线段方向,w为a1到b1的向量 double ux = a2.X - a1.X; double uy = a2.Y - a1.Y; double vx = b2.X - b1.X; double vy = b2.Y - b1.Y; double wx = a1.X - b1.X; double wy = a1.Y - b1.Y; double a = ux * ux + uy * uy; // u·u(a线段长度平方) double b = ux * vx + uy * vy; // u·v(方向向量点积) double c = vx * vx + vy * vy; // v·v(b线段长度平方) double d = ux * wx + uy * wy; // u·w double e = vx * wx + vy * wy; // v·w double denominator = a * c - b * b; // 分母:(u·u)(v·v)-(u·v)² double t, s; // 情况1:线段平行或共线(分母为0) if (Math.Abs(denominator) < 1e-9) { t = 0; // a线段起点投影到b线段 s = (b * t + e) / c; // 计算s(若c=0则b线段为点) } // 情况2:线段不平行,计算参数t和s else { t = (b * e - c * d) / denominator; // 投影参数t(a线段上) s = (a * e - b * d) / denominator; // 投影参数s(b线段上) } // 限制t和s在[0,1]范围内(超出则取端点) t = Math.Max(0, Math.Min(1, t)); s = Math.Max(0, Math.Min(1, s)); // 计算投影点距离 double dx = (wx + ux * t - vx * s); double dy = (wy + uy * t - vy * s); return Math.Sqrt(dx * dx + dy * dy); } private static double PointToSegmentDistance(IPoint p, IPoint segStart, IPoint segEnd) { double segX = segEnd.X - segStart.X; double segY = segEnd.Y - segStart.Y; if (segX == 0 && segY == 0) // 线段为点 return Math.Sqrt(Math.Pow(p.X - segStart.X, 2) + Math.Pow(p.Y - segStart.Y, 2)); double t = ((p.X - segStart.X) * segX + (p.Y - segStart.Y) * segY) / (segX * segX + segY * segY); t = Math.Max(0, Math.Min(1, t)); // 投影参数限制在[0,1] double projX = segStart.X + t * segX; double projY = segStart.Y + t * segY; return Math.Sqrt(Math.Pow(p.X - projX, 2) + Math.Pow(p.Y - projY, 2)); } /// /// 预处理环的顶点坐标(结构体数组和IPoint数组) /// /// 环的顶点集合(IPoint类型) /// 有效顶点数(闭合环需排除最后一个重复点) /// 元组:(顶点坐标结构体数组, IPoint对象数组);若异常则返回(null, null) private static ((double X, double Y)[], IPoint[]) PreprocessRingPoints(IPointCollection points, int effectivePointCount) { try { var ringPoints = new (double X, double Y)[effectivePointCount]; // 存储顶点坐标(结构体,轻量级) var ringPointsIPoint = new IPoint[effectivePointCount]; // 存储IPoint对象(用于几何操作) // 遍历有效顶点,填充两个数组 for (int k = 0; k < effectivePointCount; k++) { IPoint p = points.get_Point(k); // 获取第k个顶点 ringPoints[k] = (p.X, p.Y); // 转换为结构体 ringPointsIPoint[k] = p; // 保留IPoint对象 } return (ringPoints, ringPointsIPoint); } catch (Exception ex) { // 异常处理:顶点读取失败时记录日志(实际开发中建议添加日志记录) return (null, null); } } /// /// 判断两个向量是否同向 /// /// 向量1(如AB) /// 向量2(如CD) /// 浮点数精度容差(默认1e-6) /// 是否同向 public static bool AreVectorsSameDirection(Vector2D vec1, Vector2D vec2, double tolerance = 1e-6) { // 1. 判断共线性:叉积的绝对值小于容差(考虑浮点数精度) double crossProduct = vec1.X * vec2.Y - vec2.X * vec1.Y; if (Math.Abs(crossProduct) > tolerance) return false; // 不共线,直接返回false // 2. 判断同向性:点积大于0(且向量模长不为0,避免零向量干扰) double dotProduct = vec1.X * vec2.X + vec1.Y * vec2.Y; double length1 = CalculateVectorLength(vec1); double length2 = CalculateVectorLength(vec2); // 模长为0的向量无方向(实际线段向量不会为零,此处为安全校验) if (length1 < tolerance || length2 < tolerance) return false; return dotProduct > tolerance; // 点积为正,方向相同 } /// /// 计算向量模长 /// public static double CalculateVectorLength(Vector2D vector) { return Math.Sqrt(Math.Pow(vector.X, 2) + Math.Pow(vector.Y, 2)); } /// /// 计算线段向量 /// /// 线段起点 /// 线段终点 /// 向量 public static Vector2D CalculateVector(IPoint startPoint, IPoint endPoint) { if (startPoint == null || endPoint == null) throw new ArgumentNullException("点对象不能为空"); return new Vector2D( endPoint.X - startPoint.X, endPoint.Y - startPoint.Y ); } /// /// 计算两个向量的夹角(角度制,0°~180°) /// /// 向量1 /// 向量2 /// 精度容差(默认1e-6) /// 夹角角度(°) public static double CalculateAngle(Vector2D vec1, Vector2D vec2, double tolerance = 1e-6) { // 计算模长 double len1 = CalculateVectorLength(vec1); double len2 = CalculateVectorLength(vec2); // 处理零向量(避免除零异常) if (len1 < tolerance || len2 < tolerance) throw new ArgumentException("向量模长不能为零"); // 计算点积 double dotProduct = vec1.X * vec2.X + vec1.Y * vec2.Y; // 计算cosθ(点积/(模长乘积)) double cosTheta = dotProduct / (len1 * len2); // 修正数值精度误差(cosθ范围需在[-1, 1]内) cosTheta = Math.Max(Math.Min(cosTheta, 1.0), -1.0); // 计算弧度并转换为角度 double radians = Math.Acos(cosTheta); return radians * (180 / Math.PI); // 弧度转角度 } // 定义向量结构体 public struct Vector2D { public double X; public double Y; public Vector2D(double x, double y) { X = x; Y = y; } } #region 节点去重 private static void RemoveDuplicatePointsInRing(IRing ring) { IPointCollection pointColl = ring as IPointCollection; if (pointColl == null || pointColl.PointCount <= 1) return; // 步骤1:提取所有点到临时列表 List allPoints = new List(); for (int i = 0; i < pointColl.PointCount; i++) { allPoints.Add(pointColl.get_Point(i)); } // 步骤2:全局去重(支持X/Y/Z/M全维度比较) HashSet seenCoordinates = new HashSet(); List uniquePoints = new List(); foreach (IPoint point in allPoints) { // 生成坐标唯一键(根据需求调整是否包含Z/M) string coordKey = GetCoordinateKey(point); if (!seenCoordinates.Contains(coordKey)) { seenCoordinates.Add(coordKey); uniquePoints.Add(point); } } // 步骤3:重建点集合(先清空再添加去重点) pointColl.RemovePoints(0, pointColl.PointCount); // 清空原始点 foreach (IPoint uniquePoint in uniquePoints) { pointColl.AddPoint(uniquePoint); } // 步骤4:确保环闭合(最后一点与第一点一致) ring.Close(); } // 生成坐标唯一键(可根据需求调整比较维度) private static string GetCoordinateKey(IPoint point) { // 仅比较X/Y(忽略Z/M) double x = point.X; double y = point.Y; return $"{x:F6},{y:F6}"; // 保留6位小数避免精度问题 } #endregion /// /// 判断图形是否存在狭长 /// /// /// private static string ISNarrow(IGeometry geometry) { try { var strgeometry = GeometryConvertHelper.ConvertIGeoemtryToWKT(geometry); NetTopologySuite.Geometries.Geometry ntsGeometry = (NetTopologySuite.Geometries.Geometry)new WKBReader().Read(GeometryConvertHelper.ConvertGeometryToWKB(geometry)); // 内缩参数(侵蚀) var innerBufferParams = new BufferParameters { QuadrantSegments = 30, // 内缩平滑度 JoinStyle = JoinStyle.Bevel, // 斜角连接避免尖锐残留 EndCapStyle = EndCapStyle.Square // 平端帽处理开放线段 }; var ntsGeometry1 = ntsGeometry.Buffer(-0.05, innerBufferParams); // 内缩0.1 strgeometry = ntsGeometry1.AsText(); if (strgeometry == "POLYGON EMPTY") { return strgeometry; } // 外扩参数(膨胀) var outerBufferParams = new BufferParameters { QuadrantSegments = 200, // 高平滑度外扩 JoinStyle = JoinStyle.Mitre, MitreLimit = 2 // 限制尖锐拐角延伸长度 }; var ntsGeometry2 = ntsGeometry1.Buffer(0.05, outerBufferParams); // 外扩0.1 strgeometry = ntsGeometry2.AsText(); double length1 = ntsGeometry.Length; int pointCount1 = ntsGeometry.NumPoints; double length2 = ntsGeometry2.Length; int pointCount2 = ntsGeometry2.NumPoints; double delta_length = length1 - length2; double avg_halfangle = 180 * (pointCount1 - 1 - 2) / (pointCount1 - 1) / 2; double conner_normal_length = 2 * 0.05 / Math.Tan(avg_halfangle * (Math.PI / 180)); if (delta_length > 8 * conner_normal_length * (pointCount1 - 1)) { return strgeometry; } } catch (Exception ex) { } return string.Empty; } } public class EarClipper { // 三角剖分结果(每个三角形为3个点的集合) public List> Triangles { get; private set; } public EarClipper() { Triangles = new List>(); } // 多边形整体方向(顺时针/逆时针) private bool _isCounterClockwise; /// /// 对多边形执行耳切法三角剖分 /// /// 输入多边形(需为简单多边形,无自交) /// 三角剖分结果 public bool Clip(IPolygon polygon) { if (polygon == null || polygon.IsEmpty) return false; // 提取多边形外环的顶点(简单多边形默认处理外环) IPointCollection pointCollection = polygon as IPointCollection; if (pointCollection == null || pointCollection.PointCount < 3) return false; // 转换为顶点列表(排除最后一个闭合点) List vertices = new List(); for (int i = 0; i < pointCollection.PointCount - 1; i++) { IPoint p = pointCollection.get_Point(i); vertices.Add(p); } // 关键:计算多边形整体方向(顺时针/逆时针) _isCounterClockwise = IsPolygonCounterClockwise(vertices); // 执行耳切法 Triangles.Clear(); ClipEars(vertices); return Triangles.Count > 0; } /// /// 迭代切割耳 /// private void ClipEars(List vertices) { if (vertices.Count < 3) return; int index = 0; while (vertices.Count > 3) { // 循环取顶点(处理索引越界) int prevIndex = (index - 1 + vertices.Count) % vertices.Count; int currIndex = index % vertices.Count; int nextIndex = (index + 1) % vertices.Count; IPoint prev = vertices[prevIndex]; IPoint curr = vertices[currIndex]; IPoint next = vertices[nextIndex]; // 检查当前三点是否构成"耳" if (IsConvex(prev, curr, next) && IsEar(prev, curr, next, vertices)) { // 记录三角形 Triangles.Add(new List { prev, curr, next }); // 移除当前顶点(切割耳) vertices.RemoveAt(currIndex); // 重置索引(从下一个顶点开始检查) index = 0; } else { index++; // 防止死循环(理论上简单多边形必然有耳) if (index >= vertices.Count * 2) break; } } // 添加最后一个三角形 if (vertices.Count == 3) { Triangles.Add(new List { vertices[0], vertices[1], vertices[2] }); } } /// /// 判断顶点curr是否为凸点(通过叉积) /// private bool IsConvex(IPoint prev, IPoint curr, IPoint next) { // 计算向量:prev->curr 和 curr->next double v1x = curr.X - prev.X; double v1y = curr.Y - prev.Y; double v2x = next.X - curr.X; double v2y = next.Y - curr.Y; // 叉积(z分量):v1 × v2 = v1x*v2y - v1y*v2x double cross = v1x * v2y - v1y * v2x; // 叉积≥0:逆时针多边形的凸点(根据多边形方向调整符号) // 关键:根据多边形整体方向判断凸点 if (_isCounterClockwise) { // 逆时针多边形:叉积 >= 0 为凸点 return cross >= 0; } else { // 顺时针多边形:叉积 <= 0 为凸点(与逆时针相反) return cross <= 0; } } /// /// 判断多边形顶点是顺时针还是逆时针排列 /// private bool IsPolygonCounterClockwise(List vertices) { double sum = 0; int n = vertices.Count; for (int i = 0; i < n; i++) { IPoint p = vertices[i]; IPoint q = vertices[(i + 1) % n]; // 下一个顶点(首尾相连) sum += (q.X - p.X) * (q.Y + p.Y); } // sum > 0 表示逆时针,sum < 0 表示顺时针 return sum > 0; } /// /// 判断三点组成的三角形是否为"耳"(内部无其他顶点) /// private bool IsEar(IPoint a, IPoint b, IPoint c, List vertices) { // 检查所有其他顶点是否在三角形abc内部 foreach (IPoint p in vertices) { // 跳过a、b、c本身 if (p.Equals(a) || p.Equals(b) || p.Equals(c)) continue; // 判断点p是否在三角形abc内部 if (IsPointInTriangle(p, a, b, c)) return false; } return true; } /// /// 判断点p是否在三角形abc内部( barycentric坐标法) /// private bool IsPointInTriangle(IPoint p, IPoint a, IPoint b, IPoint c) { double v0x = c.X - a.X; double v0y = c.Y - a.Y; double v1x = b.X - a.X; double v1y = b.Y - a.Y; double v2x = p.X - a.X; double v2y = p.Y - a.Y; // 计算点积 double dot00 = v0x * v0x + v0y * v0y; double dot01 = v0x * v1x + v0y * v1y; double dot02 = v0x * v2x + v0y * v2y; double dot11 = v1x * v1x + v1y * v1y; double dot12 = v1x * v2x + v1y * v2y; // 计算 barycentric坐标 double invDenom = 1.0 / (dot00 * dot11 - dot01 * dot01); double u = (dot11 * dot02 - dot01 * dot12) * invDenom; double v = (dot00 * dot12 - dot01 * dot02) * invDenom; // 点在三角形内(包括边界) return (u >= 0) && (v >= 0) && (u + v <= 1); } } }