数学篇 cad.net 射线法点在多段线内_判断四个点构成矩形
点在多段线内
轴向矩形
矩形只是多段线的一种解
轴向矩形(非旋转的矩形)可以利用坐标直接相减(速度最快),
因为CPU每秒50亿次计算,因此AABB包围盒作为四叉树节点,是非常快的.
同时注意类的实现,因为WPF的类有冗余设计,不太适合快速场景,需要自己构造一个.见四叉树
非轴向矩形(带旋转)
非轴向矩形可以利用叉乘求解.
举个例子,不完全的代码: R1,R2,R3,R4是矩形的角点
/// <summary>
/// 点在矩形内为<see langword="true"/>
/// </summary>
/// <returns></returns>
public bool Contains(PointV p)
{
return R1.Cross(R2, p) * R3.Cross(R4, p) >= 0 &&
R2.Cross(R3, p) * R4.Cross(R1, p) >= 0;
}
多段线_射线法
测试命令
public partial class CmdTest
{
[CommandMethod("CmdTest_InBoundary")]
public void CmdTest_InBoundary()
{
var dm = Acap.DocumentManager;
var doc = dm.MdiActiveDocument;
var db = doc.Database;
var ed = doc.Editor;
ed.WriteMessage(Environment.NewLine + "射线法测试:");
var ppo = new PromptPointOptions(Environment.NewLine + "测试点:")
{
AllowArbitraryInput = true,//任意输入
AllowNone = true //允许回车
};
var ppr = ed.GetPoint(ppo);//用户点选
if (ppr.Status != PromptStatus.OK)
return;
db.Action(tr => {
var peo = new PromptEntityOptions(Environment.NewLine + "点选多段线")
{
AllowObjectOnLockedLayer = false,
AllowNone = false
};
var ent2 = ed.WhileEntsel(tr, peo, null, new EntityType[] { EntityType.Polyline });
if (ent2 is ObjectId id && id.IsOk())
{
var ent = id.ToEntity(tr);
if (ent is Polyline pl && ppr.Value.InBoundary(pl.GetEntityPoint3ds().ToArray()))
ed.WriteMessage(Environment.NewLine + "内内内内内内内内");
else
ed.WriteMessage(Environment.NewLine + "外外外外外外外外外外外");
}
});
}
}
子函数
public static partial class MathHelper
{
/// <summary>
/// 点在闭合多段线内,水平射线法
/// </summary>
/// <param name="p">判断的点</param>
/// <param name="ptsArr">边界点集</param>
/// <param name="onBoundary">在边界上算不算</param>
/// <param name="tolerance">容差</param>
/// <returns></returns>
public bool InBoundary(this Point3d p, Point3d[] ptsArr, bool onBoundary = true, double tolerance = 1e-6)
{
// 首尾相连
static void End2End<T>(List<T> lst)
{
if (!lst[0].Equals(lst[lst.Count - 1]))
lst.Add(lst[0]);
}
var pts = ptsArr.ToList();
End2End(pts);
return InBoundary(p, pts, onBoundary, tolerance);
}
/// <summary>
/// 点在闭合多段线内,水平射线法
/// <a href="https://www.cnblogs.com/anningwang/p/7581545.html">原文链接</a>
/// </summary>
/// <param name="p">判断的点</param>
/// <param name="pts">边界点集</param>
/// <param name="onBoundary">在边界上算不算</param>
/// <param name="tolerance">容差</param>
/// <returns></returns>
public bool InBoundary(this Point3d p,List<PointV>? pts, bool onBoundary = true, double tolerance = 1e-6)
{
bool Eq(double a, double b)
{
return Math.Abs(a - b) <= tolerance;
}
if (pts is null)
throw new ArgumentNullException(nameof(pts));
var testx = p.X;
var testy = p.Y;
//下面边线碰撞合并可以提速
//轴向矩形范围内
var (min, max) = Rect.GetMinMax(pts);
if (testx < min.X || testx > max.X || testy < min.Y || testy > max.Y)
return false;
//点==边点(因为直接分析多段线会导致下是上否,所以这里统一处理)
for (int v = 0; v < pts.Count; v++)
{
if (Eq(pts[v].X, testx) && Eq(pts[v].Y, testy))
{
if (onBoundary)
return true;
else
return false;
}
}
//分析多段线
var flag = false;
int nvert = pts.Count;
var vertx = pts.Select(a => a.X).ToArray();
var verty = pts.Select(a => a.Y).ToArray();
int i = 0, j = 0;
for (i = 0, j = nvert - 1; i < nvert; j = i++)
{
if (((verty[i] > testy) != (verty[j] > testy)) &&
(testx < (vertx[j] - vertx[i]) * (testy - verty[i]) / (verty[j] - verty[i]) + vertx[i]))
flag = !flag;
}
return flag;
}
/// <summary>
/// 点在闭合多段线内,水平射线法
/// </summary>
/// <param name="p">判断的点</param>
/// <param name="pts">边界点集</param>
/// <param name="onBoundary">在边界上算不算</param>
/// <param name="tolerance">容差</param>
/// <returns></returns>
public bool InBoundary2(this Point3d p,List<PointV>? pts, bool onBoundary = true, double tolerance = 1e-6)
{
bool Eq(double a, double b)
{
return Math.Abs(a - b) <= tolerance;
}
if (pts is null)
throw new ArgumentNullException(nameof(pts));
var flag = false;
var testx = p.X;
var testy = p.Y;
//轴向矩形范围内
var (min, max) = Rect.GetMinMax(pts);
if (testx < min.X || testx > max.X || testy < min.Y || testy > max.Y)
return false;
for (var i = 0; i < pts.Count - 1; i++)
{
var x1 = pts[i]._X;//头
var y1 = pts[i]._Y;
var x2 = pts[i + 1]._X;//尾
var y2 = pts[i + 1]._Y;
// 点==边点
if ((Eq(x1, testx) && Eq(y1, testy)) || Eq(x2, testx) && Eq(y2, testy))
{
flag = true;
break;
}
// 子段端点是否在水平射线两侧(都在下面 || 都在上面)
if ((y1 < testy && y2 >= testy) || (y1 >= testy && y2 < testy))
{
//射线穿过子段时,得出交点为(ox,y)
var derivative = (x2 - x1) / (y2 - y1); //导数.斜率
var high = testy - y1; //高
var ox = x1 + high * derivative;
// 点在多边形的边上
if (Eq(ox, testx) && onBoundary)
{
flag = true;
break;
}
// 射线穿过多边形的边界
if (ox > testx && onBoundary)
flag = !flag;
}
}
return flag; // 射线穿过多边形边界的次数为奇数时点在多边形内
}
}
判断四个点是否构成矩形(带旋转)
测试命令
namespace JoinBox
{
public partial class CmdTest
{
[CommandMethod("CmdTest_IsRect")]
public void CmdTest_IsRect()
{
var dm = Acap.DocumentManager;
var doc = dm.MdiActiveDocument;
var db = doc.Database;
var ed = doc.Editor;
ed.WriteMessage("\n判断四个点是否构成矩形(带旋转)");
if (false)
{
var pts = new List<PointV>();
for (int i = 0; i < 4; i++)
{
var pr = ed.GetPoint("\n 选择点");
if (pr.Status != PromptStatus.OK)
return;
pts.Add(pr.Value);
}
//预先揭露答案
ed.WriteMessage("\n 是矩形吗?:" + RectHelper.IsRect(pts, 8));
return;
}
PromptEntityOptions peo = new("\n选择多段线:");
peo.SetRejectMessage("\n必须是多段线!");
peo.AddAllowedClass(typeof(Polyline), false);
var per = ed.GetEntity(peo);
if (per.Status != PromptStatus.OK)
return;
db.Action(tr => {
var pwl = per.ObjectId.ToEntity(tr) as Polyline;
if (pwl == null)
return;
var pts = new List<PointV>();
pwl.GetEntityPoint3ds().ForEach(a => pts.Add(a));
bool isRect = false;
var t1 = TimeHelper.RunTime(() => {
for (int i = 0; i < 100_0000; i++)//一百万次
{
isRect = RectHelper.IsRect(pts, 2);
if (isRect)
{
}
}
});
Debug.WriteLine($"\n 山人妙计法:{isRect}");
ed.WriteMessage($"\n 山人妙计法:{t1}");
var t2 = TimeHelper.RunTime(() => {
for (int i = 0; i < 100_0000; i++)//一百万次
{
isRect = RectHelper.IsRect(pts, 4);
if (isRect)
{
}
}
});
Debug.WriteLine($"\n 点乘求点法:{isRect}");
ed.WriteMessage($"\n 点乘求点法:{t2}");
var t3 = TimeHelper.RunTime(() => {
for (int i = 0; i < 100_0000; i++)//一百万次
{
isRect = RectHelper.IsRect(pts, 8);
if (isRect)
{
}
}
});
Debug.WriteLine($"\n 点乘求值法:{isRect}");
ed.WriteMessage($"\n 点乘求值法:{t3}");
});
}
}
}
子函数
数学库另见博客首页置顶点乘
namespace JoinBox
{
public static partial class RectHelper
{
/// <summary>
/// 是否矩形(带角度)
/// </summary>
/// <param name="ptList"></param>
/// <returns></returns>
public static bool IsRect(List<PointV>? ptList, int actionNum = 2)
{
if (ptList == null)
throw new ArgumentNullException(nameof(ptList));
var pts = ptList.ToList();
if (ptList.Count == 5)
{
//首尾点相同移除最后
if (pts[0].IsEqualTo(pts[pts.Count - 1], 1e-6))
pts.RemoveAt(pts.Count - 1);
}
if (pts.Count != 4)
return false;
if ((actionNum & 2) == 2)
{
//山人方案:对角线等长及一个角90度
//对角长度相等(矩形/梯形(有第二个解,因此需要90度))
var length1 = pts[0].GetDistanceTo(pts[2]);
var length2 = pts[1].GetDistanceTo(pts[3]);
if (Math.Abs(length1 - length2) < 1e-10)
{
var v1 = pts[0].GetVectorTo(pts[1]);
var v2 = pts[0].GetVectorTo(pts[3]);
return Math.Abs(v1.GetAngleTo(v2) - Constant.PiHalf) < 1e-10;
}
return false;
}
if ((actionNum & 4) == 4)
{
//点乘求点法:
//90度的话,点乘ABD==A;若BC斜边,导致前面点乘不足,则再点乘BCA==B
//时间上面:点乘内部用了"求距离"求GetUnitNormal(牛顿迭代),两次则*2了.
//点乘会new点,因此更慢
var P1 = pts[0].DotProduct(pts[1], pts[3]);
var P2 = pts[1].DotProduct(pts[2], pts[0]);
return P1.IsEqualTo(pts[0], 1e-10) && P2.IsEqualTo(pts[1], 1e-10);
}
if ((actionNum & 8) == 8)
{
//点乘求值法:(为了处理 正梯形/平行四边形 需要三次)
var dot = pts[0].DotProductValue(pts[1], pts[3]);
if (Math.Abs(dot) < 1e-8)
{
dot = pts[1].DotProductValue(pts[2], pts[0]);
if (Math.Abs(dot) < 1e-8)
{
dot = pts[2].DotProductValue(pts[3], pts[1]);
return Math.Abs(dot) < 1e-8;
}
}
}
return false;
}
}
}
为什么需要三次,而不是对角90度,因为有这样的情况:
结果
(完)