数学篇 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 + "外外外外外外外外外外外");
            }
        });
    }
}

子函数

GetEntityPoint3ds

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度,因为有这样的情况:

结果

(完)

posted @ 2020-11-30 16:26  惊惊  阅读(1260)  评论(0编辑  收藏  举报