cad.net bo边界算法

发送lisp

(bpoly point)

发送命令

Acad的低版本只能发送命令的方式获取边界信息,edata说这个bo是写在arx内的,不是接口,所以也不能反射用(没深究了)

低版本:
通过命令发送bo的方法:发送bo命令例子

要注意的是,如果空格再次执行上次命令,你会执行到bo,而不是你的当前命令.
而正常的cad用户都是要执行当前的命令.
例如填充命令发送bo命令,那么再次空格是bo,而用户需要是填充

解决方法:要解开文档锁的情况下发送一次当前命令,而且在命令函数外做一个立即结束的标记,
在最前面写:
img

到了Acad2011: 就可以通过以下语句获取

Editor ed = Acap.DocumentManager.MdiActiveDocument.Editor;
ed.TraceBoundary(........);

算法概念

但是,要是自己能造一个边界算法,为什么要靠桌子的.

打倒自动桌子!!!! 打倒Acad!!!

img

分析桌子的bo算法

桌子的bo算法原理:
先获取当前屏幕可视的图形,然后用的是像素一点一点抓边界像素(填充法),所以非常快.

知道了之后,就明白这是一个前台操作,而我们需要挑战的是后台bo算法,也就是无像素的前提下分析边界.
而且必须要做得很快,不然就没有意义啦.

方案一,单点射线法(实际上就是夹角左转法)

于是乎,我做了一张图(拖拉图片看大图)

不要问为什么是奇点不是起点,因为起点的意义太多了,换个字免得引起歧义而已(用原点又会跟世界坐标原点重意辣!像爆炸一样辐射获取收到冲击波的边界).

img

1, 减少遍历,不可能O(n²)比较的,所以我们利用四叉树
制作一个查找临近图元的功能,这个比较简单,我都写完了.

2, 抛弃非边界计算图元,例如文字,标注,引线.
如果奇点,不在块的嵌套层次上,那么表示块边界才是遍历的,(此边界相当于bv树的外层)
如果在嵌套层次里,先算层次内,边界没密封才继续遍历其他块边界.

下面是山人告诉我的bo算法思想... 它就是图上的拆解算法.

样图

img

全部交点打断,然后bo到的边界就是我标数字的线段组成的.(其实不是打断,是求交点,我是便于大家理解)

步骤如下

0x01 射线求交点

img

从A点做0°直线(射线),与所有的线只有4个交点,就是我画绿色圆的点;

选择一个离A点最近的点,并通过这个点得到1号直线: 数学上来讲就是两个直线方程联立.

0x02 获取下一段线段

img

得到1号直线的两个端点,选择相对A-1线角度逆时针方向的点C,

由C点做选择集,得到2号和1号直线,排除1号直线:
可能C点相交有多个图元,存在和 0x03 一样的情况,可能存在没有相交的图元,需要细节处理.

然后用2号直线的另一个交点d做选择集:
非选择集需要轮询(所有图元? 2号线包围盒范围内的?) 与2号线来求交,得出最近交点,

得到2号、3号,21、22号直线.(如果是不打断的话,则没有21、22,但是,万一情况是图元接上去呢,则需要以相同条件来进行)

0x03 逆时针选择的作用

这个时候,你要选择3号直线才是对的,

算法应选择直线的角度更靠近逆时针的那个: 也就是说d为基点,22号 在最右,21在中间,3在最左,那就选择最左的那个3

这时候的你轮廓集合里已经有3条直线了,分别为1 、2 、 3

0x04 循环以上操作

然后就是不断的重复,直到走到12号线,又一次选择到了1号线,循环结束;

分析

经过0x010x02的步骤可知,每次都需要轮询一次所有的图元,那么这个轮询就是数量就是线性递减的,因为已经分析过的图元,除了第一个需要保留以判断闭环之外,其他都可以抛弃.

这个是最简单的,没有内部孤岛,没有曲线,的bo原理.

方案二

我们发现上面的算法它在计算单一闭合好用,但是多闭合就有问题.

于是乎找到了此论文,主要是利用图来实现.
图分为邻接表和邻接矩阵.
当模型比较小的时候,用矩阵有优化效率.原因是邻接表是内存离散模型,矩阵是连续的(内存不存在多维数组,全是一维模拟的),而我们需要高频访问矩阵,这是优化阶段的东西.
二维图形封闭区域自动识别算法

广度优先算法

根据论文,使用了一个邻接表,c#直接用词典模拟了它,
邻接表的key是交点字符串(注意要去精度),
value是节点类,类中有相遇链/坐标/颜色/步数(论文是长度,更具体见论文),
当分类完成之后,1{2,9}表示1节点同时链接着2,9节点.

论文核心是染色过程中提取了相遇链,核心是相遇时候的处理,然后回溯父节点.
其中染色的O(n)一次完成.

但是阿惊依照论文流程实现了一次代码之后,发现有蛮多点和我们想要的曲线闭合不太相同.
不好的地方是相遇点再次寻找到下一个相遇点时候,
第2,3,4...节点不能父节点回溯,而是图会呈现水波状态向外绽放,
所以论文只会呈现最后图样那样,也就是一次回溯.

也跟bo算法中直接提取所有封闭区不同...难搞噢,或许你们可以想到一个顺利回溯水波状方法的方法.😆

山人找到了这个stackoverflow:find-all-chordless-cycles

不过要注意:图的算法是抛弃双曲线闭合(单曲线闭合直接过滤图元属性就好了),这样的ab两个节点就包含一个闭合区.这也难搞噢.

此处待续...

深度优先算法

小学问题:数数里面多少个多边形...成人解决方案版.
实际上我们只需要1/2/3区.
邻接表进行深度优先是利用穷举每个节点而成立的,闭环一个,回上一个分叉路再寻.

涂色在这里可以变成:
我们遍历每个节点必然会有8字型走成b或者p,腰身闭合的情况.
那么我们每走一步就遍历一次全部腰身的话,很蛋疼,所以设计了这个涂色.
涂色也可以是个序号,例如碰到4号,尾巴到4就是一条闭合线了.
每次完成一次遍历需要把涂色全部归0.

但是我觉得这样会造成一个可怕的结果,
穷举的多段线点序是下移的,也就是:点123/231/312,然后还有双向:点321/213/132,
并且还会穷举出一些我们不要的(图上面的4/5/6区)
一条多段线就那么多了耶.这要很多个数组储存呀.判断重复也需要处理时间.

总结起来就是:
key序号就失去了角度性,得到了快捷,快捷又出现了穷举,穷举又带来了点序下移和双向和废区.

方案三

通过有限采样进行获取三角封闭区,这个方法没有进行过实践,
而且这样貌似和三维建模如出一辙.

方案四

要求孤岛怎么办?答案是上扫描线算法,在y参照线求交,和x线求交,组合成封闭区

cad的bo算法它本质上是填充算法.填充算法只有两种,一种叫种子填充算法,一种叫扫描线算法,而查找封闭区也是同一个问题.
长期以来,我都觉得cad的bo太慢了,所以我一直想对其重写,试过很多算法,之前博客的左旋和染色,发现有缺陷.
左旋法的缺点,"囙"字,一直左旋也得到"凹",要引入广度优先算法和深度优先算法(不好).而且无法分析孤岛.染色法也一样,遇到多节点岔路时候存在相同问题.
这样的话,"图节点"算法需要抛弃了,所以我们需要使用扫描线算法.
它能算多个分离的:口 口 口
也能算合并的:口二口二口
也能算孤岛的:囙回
它真的很快,不需要并行都能秒出全图所有封闭区,是一些PCB软件的基本功能.

全部流程:
第一步, 消重
第二步, 循环剪枝
第三步, 横扫,竖扫,并组
第四步, 模拟填充,分析面积提取外边界

开始分析:
我先说核心,第三步,横扫:
扫描线算法的本质是横轴求交点,排序交点具备有序性,有序性可以做什么?可以进行对边配对.

0x01,什么叫对边配对?面域(封闭区域)等于一个人,一颗子弹从前胸进入必然从后背穿出,因此面域交点必然是两两配对的.

0x02,因为有0x01的性质,一条横向参照线就是子弹,和这行每个图元求交点,交点们x排序后,按照xa-xb,xc-xd,xe-xf画线,然后不断修改xline.Y+=0.1,接着求交再画下一次的线,这就是模拟填充算法.

0x03,那么封闭区求法怎么才能不跟填充一样密集计算0.1步进呢?如何降低运算量?
图形:□▽,如果在minX位置横切一刀会得到了3个交点(奇数),□△同理.这上下两刀算一个面域,会浪费时间不是吗?
所以要算中点,在中间横切一刀就是最简单的!中点的目的就是为了利用y进行步进,而不是0.1步进.但是中间横切一刀得到的交点们,也可能存在端点(你故意画个小三角呢),因此你还需要求交点之后剔除同一行扫描图元的端点们,并且进行交点排序消重(重叠图元问题).

0x04,并组,获得填充封闭区:
上面求到交点有什么用?交点是没啥用的(除了模拟填充),但是交点击中的图元有用.
图形:口,横扫交点分别是ab(左右).竖扫是cd(下上).
交点的图元需要有一个面域链表,这样才能加入对边.
class Bo{
ObjectId id;
List Link;//面域环表
}
boEntityA.Link.Add(boEntityB);
boEntityB.Link=boEntityA.Link;
这个Link(a+b)虽然在一个面域内,但此时拐点没关联啊?它们无法闭环得到一个封闭区呀.因此需要引入竖扫得到Link(c+d).
横扫和竖扫(可以并行的,因为它是一行一列分别求交,这个特性使得它运算非常快)
横扫每组头尾点分别搜一次竖扫的头尾点,把竖扫的Link加入过来,这就是拐点关联了,也就是完成了封闭区查找!!
拐点是可能存在多个(外边界问题),但是竖扫也需要排序消重点,因此是具备唯一性,保证了填充边界的正确.

外边界问题:
图形L\7,这样横扫有abc三个交点呢,是奇数.
答案是:看一次cad的填充,最后交点再生成的一个虚拟点成为偶数,然后填充0宽边界(挺变态的,我们也可以不按照cad做法,只填充前面部分,所以扫描线算法是失去旋转性的)
填充边界就是谁先连线就谁先封闭.而外边界分析,只能查关联点的时候进行面积比较,留下大的部分.

全部封闭区:
有没有想过,xa-xb,xc-xd,xe-xf...这个操作并不是全部封闭区?只是刚开始树立了一个间隔填充算法给你?
不然你试试五芒星,会发现星星中间没有填充也是面域哦.因此填充边界组成的边界仍可能具备闭环性.
因此我们还需要xb-xc,xd-xe..看看它们是否具备闭环关系.

第二步:
图形:口口_|,这样横切一刀时候,发现末尾多了一个交点,并且影响判断,这样图形就是多了分支,因此需要剪枝:可以看见有一个节点悬空,进行循环删除.删除之后就只有"口口"了(连_也删了哦)

第一步:
其实不需要消重,因为消重也是求交,扫描线也是求交,扫完之后剔除重复交点就好了.

但是写都写了,保留这个图元消重算法吧:
获取选区图元进行两两求交.
无交点:可能是无限个,需要端点判断,判断后有交点则进入有交点环节.
有交点:打断全部图元,直断选择一边保留即可.

重合情况:
a:直线重合:包围盒完全相同,直线删剩一条.
部分重合就计算端点--
b:曲线重合:包围盒完全相同,采样份>3,逐一比较,完全一致就删剩一条.
部分重合就计算端点--
(为什么要>3,因为样条曲线3点和圆弧3点的包围盒和采样是一致的,但是图形不一致)
曲线重合不需要判断长度一样(因为耗时)只需要通过曲线定份函数得到很多采样点,排序,两个点集一一对应,就是满足重合性也满足长度一致性.(还可以用simd指令优化速度).

部分重合计算端点--如果a1端点和b线碰撞&&b1端点和a线碰撞,则可能存在直断(曲线凸起就不重合),或者a2&&b2端点条件.两线两点碰撞之后,获取两条中间段,我们再对中间段曲线采样分析,是否保留...

全图两两求交时间太慢,还记得我上一文章的快速碰撞方法吗?然后再进行碰撞组内的两两比较.
在ifox上面有个curve.ToCompositeCurve3d()函数的对象可以求曲线交点,打断它们得到碎线.
顺带一提,转为这个cc3d求交也很快(因为模拟填充是0.1步进所以我知道).
不管你这一步怎么做,都得出全部无碰撞碎线在内存里面.

我来猜想一下cad的bo为什么慢?
你会看见bo命令是需要可见即可得的.
遇到块裁剪边界它会打开显示,而不是块表获取图元运算,这说明了这是它在利用界面缓存进行种子填充算法,不断利用种子生长逼近边界.
然后当前可见屏幕部分可能导致块裁剪部分重叠,打开了每个块边界进行重绘,再进行布尔,这一步是需要把当前画面像素全部复制一次比较的.

namespace JoinBoxAcad;

using Autodesk.AutoCAD.DatabaseServices;
using System;
using System.Collections.Generic;
using System.Linq;
using Scanline = List<IntersectGroup>;
using Scanlines = List<List<IntersectGroup>>;
using SoCuvers = HashSet<CurveInfo>;


public class JTopo
{
    #region 成员
    // cad容差类
    public static readonly Tolerance CadTolerance = new(1e-6, 1e-6);
    static readonly Plane _plan = new();
    static readonly Point3dCollection _plIns = new();

    // 求交类(每次set自动重置,都会有个新的结果)
    static readonly CurveCurveIntersector3d _Cci3d = new();
    public List<CurveInfo> CurveInfos;
    #endregion

    /// <summary>
    /// 获取封闭曲线
    /// </summary>
    /// <param name="curves"></param>
    /// <exception cref="ArgumentNullException"></exception>
    public JTopo(List<Curve> curves)
    {
        if (curves == null || curves.Count == 0)
            throw new ArgumentNullException(nameof(curves));

        CurveInfos = new();
        //提取包围盒信息
        for (int i = 0; i < curves.Count; i++)
            CurveInfos.Add(new(curves[i]));
    }


    /// <summary>
    /// 利用交点断分曲线 和 独立自闭曲线
    /// </summary>
    /// <param name="scatteredEdges_out">传出不自闭的曲线集</param>
    /// <param name="closed_out">传出独立自闭曲线(孤岛)</param>
    public void GetEdgesAndnewCurves(
        List<CompositeCurve3d> scatteredEdges_out,
        List<CompositeCurve3d> closed_out)
    {
        for (int a = 0; a < CurveInfos.Count; a++)
        {
            var curve1 = CurveInfos[a];

            // b = a 第一次是自交对比,用于断分 样条曲线 闭合打圈圈(捆扎带型)
            for (int b = a; b < CurveInfos.Count; b++)
            {
                var curve2 = CurveInfos[b];

                // 包围盒没有碰撞就直接结束
                if (!curve1.IntersectsWith(curve2))
                    continue;
                //if (curve1.BoundBlock.IsDisjoint(curve2.BoundBlock))//这个也是包围盒
                //    continue;

                // 求交点,提供交点参数
                _Cci3d.Set(curve1, curve2, Vector3d.ZAxis);
                // 计算两条曲线的交点(多个),分别放入对应的交点参数集
                for (int k = 0; k < _Cci3d.NumberOfIntersectionPoints; k++)
                {
                    var pars = _Cci3d.GetIntersectionParameters(k);
                    curve1.Paramss.Add(pars[0]);//0是第一条曲线的交点参数
                    curve2.Paramss.Add(pars[1]);//1是第二条曲线的交点参数
                }
            }

            // 闭合 && 无碰撞 ; 闭合不一定和其他没有碰撞,所以需要判断有无碰撞才加入独立集合
            if (curve1.IsClosed() && curve1.Paramss.Count == 0)
                closed_out.Add(curve1);

            // 跳过独立无交点图元
            if (curve1.Paramss.Count == 0)
                continue;

            // 根据交点参数断分曲线
            var c3ds = curve1.GetSplitCurves(curve1.Paramss);
            if (c3ds != null)
            {
                scatteredEdges_out.AddRange(c3ds);
            }
            else
            {
                // 狐哥写的这里出现的条件是:有曲线参数,但是切分不出来曲线...没懂为什么...
                // 是这些参数?{参数0位置?头参/尾参/参数不在曲线上?}
                // scatteredEdges_out.Add(curve1);
                Debugger.Break();
            }

            //  有交点的才消重,无交点必然不重复
            CurveInfo.Distinct(scatteredEdges_out);
        }
    }

    /// <summary>
    /// 构造邻接表并修剪单一分支
    /// </summary>
    /// <param name="scatteredEdges_inOut">传入每组有碰撞的(零散的肯定碰撞)</param>
    /// <param name="closed_out">传出自闭曲线集</param>
    public static void PruneBranch(List<CompositeCurve3d> scatteredEdges_inOut,
                                   List<CompositeCurve3d> closed_out)
    {
        // 移除自闭曲线
        List<CompositeCurve3d> removeClosed = new();
        // 邻接表<共点,边集合>
        Dictionary<string, HashSet<CompositeCurve3d>> dict = new();
        // Dictionary<string, HashSet<CurveInfo>> dict = new();

        // 遍历边图元,构造邻接表
        for (int i = 0; i < scatteredEdges_inOut.Count; i++)
        {
            var ce = scatteredEdges_inOut[i];
            // 曲线闭合直接提供出去
            if (ce.IsClosed())
            {
                removeClosed.Add(ce);
                continue;
            }

            // 交点
            var spY = ce.StartPoint.GetHashString(2);
            var epY = ce.EndPoint.GetHashString(2);

            HashSet<CompositeCurve3d> spNode;
            HashSet<CompositeCurve3d> epNode;
            if (dict.ContainsKey(spY))
                spNode = dict[spY];
            else
                spNode = new();//ce.StartPoint

            if (dict.ContainsKey(epY))
                epNode = dict[epY];
            else
                epNode = new();//ce.EndPoint

            // 加入边图元(旧表直接加入)
            if (!spNode.Contains(ce))
                spNode.Add(ce);
            if (!epNode.Contains(ce))
                epNode.Add(ce);

            // 新的元素加入边表
            if (!dict.ContainsKey(spY))
                dict.Add(spY, spNode);
            if (!dict.ContainsKey(epY))
                dict.Add(epY, epNode);
        }

        // 剪枝
        var dictValues = dict.Select(a => a.Value).ToList();
        if (dictValues.Count != 0)
            PrunBranch(dictValues, sub => { scatteredEdges_inOut.Remove(sub); });

        /*
         * 剪枝之后的获取凸包
         * 0x01 分离凸包关系,制作凸包包含树: 多叉树,凸包分离就是并排,否则就是包含
         *      邻接表如何区分不同的凸包:
         *      制作一个集合储存当前染色号,
         *         遍历邻接表成员,
         *              如已染色,取小的色号替换并移除集合上面大的色号,
         *              如无染色,从集合上面尾巴++,写入节点.
         *      现在有几个色号就是几个凸包.
         * 0x02 独立的凸包,进行扫描线.
         * 0x03 非独立的凸包,将下面所有的凸包加一起,进行扫描线
         * 为了两个目型带角度图元头部连接封闭.同水平线交点分析奇偶数时,需要获取同凸包(染色号)左右两边.为了两个日型带角度图元头部连接封闭.
         */


        // 移除自闭曲线,加入自闭集合
        for (int i = 0; i < removeClosed.Count; i++)
        {
            closed_out.Add(removeClosed[i]);
            scatteredEdges_inOut.Remove(removeClosed[i]);
        }
    }

    /// <summary>
    /// 剪枝:修剪邻接表单一分支
    /// </summary>
    /// <param name="dict">邻接表</param>
    /// <param name="remove">输出剔除的对象</param>
    static void PrunBranch<Tcc3d>(List<HashSet<Tcc3d>> dictList, Action<Tcc3d> remove)
    {
        // 剪枝:
        // 多个图元组成的闭合曲线至少有两条曲线通过,而共点若只有一次使用,代表此图元是尾巴
        // 循环剪枝,因为移除尾巴后的仍然可能是尾巴
        while (true)
        {
            var polyss = dictList.Where(b => b.Count < 2/*只有一个点就是尾巴*/).ToList();
            if (polyss.Count == 0)
                break;
            for (int i = 0; i < polyss.Count; i++)
            {
                var sub = polyss[i].First();// 只有一个所以只需要取出一个
                remove?.Invoke(sub);

                // 遍历其他剔除同一个成员,以此制造链式删除
                for (int v = dictList.Count - 1; v >= 0; v--)
                {
                    dictList[v].Remove(sub);
                    if (dictList[v].Count == 0)
                        dictList.RemoveAt(v);
                }

                // 清理并移除
                polyss[i].Clear();
                dictList.Remove(polyss[i]);
            }
        }
    }

    /// <summary>
    /// 模拟填充
    /// </summary>
    public static void SimulateHatch(List<CompositeCurve3d> cc3ds, DBTrans? tr = null)
    {
        tr ??= DBTrans.Top;

        // 曲线集合
        List<CurveInfo> curveInfos = new();
        for (int i = 0; i < cc3ds.Count; i++)
            curveInfos.Add(new(cc3ds[i]));

        // 包围盒排序,防止合并图元时候出现非顺序合并
        curveInfos = curveInfos.OrderBy(a => a._Y).OrderBy(a => a._X).ToList();

        // 获取所有包围盒的最上最下
        double maxY = double.MinValue;
        double minY = double.MaxValue;
        for (int i = 0; i < curveInfos.Count; i++)
        {
            maxY = curveInfos[i]._Top > maxY ? curveInfos[i]._Top : maxY;
            minY = curveInfos[i]._Y < minY ? curveInfos[i]._Y : minY;
        }
        if (maxY - minY <= 0)
            return;

        // 线性步进
        var det = (maxY - minY) / 1000;
        if (det <= 0)
            return;

        // 为了保证每个曲线仅加入一组
        Dictionary<CurveInfo, SoCuvers> groups = new();
        Scanlines yxlines = new();
        LineSegment3d xline;

        minY += det;  // 跳过最下面
        for (var aset = minY; aset < maxY; aset += det)
        {
            // 一条X扫描线的交点
            Scanline xlines = new();

            // 因为已经按照Y排序,所以并不会遍历全部图元,但是前面的图元仍然会重复过滤
            for (int i = 0; i < curveInfos.Count; i++)
            {
                // X扫描线在包围盒范围
                if (curveInfos[i]._Y <= aset && aset <= curveInfos[i]._Top)
                {
                    // 用于求交的参照线,也就是X扫描线
                    xline = new LineSegment3d(
                      new Point3d(curveInfos[i]._X - 1/*1是容差*/, aset, 0),
                      new Point3d(curveInfos[i]._Right + 1/*1是容差*/, aset, 0));

                    _plIns.Clear();
                    IntersectWith(xline, curveInfos[i]);
                    if (_plIns.Count != 0)
                    {
                        if (_plIns.Count == 1)
                        {
                            // 样条曲线 闭合打圈圈(捆扎带型),它的包围盒没有逼近,会在外部,
                            // 所以可能获取单个点,跳过此情景的加入,避免误认为是奇数点
                            if (curveInfos[i].IsCurve)
                                continue;
                        }
                        else
                        {
                            //if (false) // 不用修正,通过上面曲线类型,直接跳过就好了
                            //    if (_plIns.Count == 2 && curveInfos[i].Curve is Spline)
                            //    {
                            //        // 单一样条曲线封闭,修正包围盒范围
                            //        curveInfos[i]._Y = aset;
                            //        xlines.Add(new(_plIns[0].X, curveInfos[i]));
                            //        xlines.Add(new(_plIns[1].X, curveInfos[i]));
                            //        continue;
                            //    }
                        }

                        // 单一样条曲线封闭有多个交点
                        // 获取交点的时候将X记录在曲线上面
                        foreach (var pt in _plIns)
                        {
                            CreateOrGetGroup(curveInfos[i], groups);
                            xlines.Add(new(pt.X, curveInfos[i]));
                        }
                    }
                }
            }

            // 因为是两两配对,所以它为偶数才是对的
            if ((xlines.Count % 2) != 0)
            {
                Env.Printl("边界错误");
                return;
            }

            // 横向排序
            if (xlines.Count > 0)
                yxlines.Add(xlines.OrderBy(ig => ig.XorY).ToList());
        }

        // 画线,模拟填充,两两配对的
        var asetY = minY;
        APairTask(yxlines, (left, right) => {
            tr.CurrentSpace.AddLine(new(left.XorY, asetY, 0), new(right.XorY, asetY, 0));
        }, () => { asetY += det; });
    }


    readonly static List<double> _parList = new();

    /// <summary>
    /// 求交点
    /// </summary>
    /// <param name="xline">模拟参照线,在图元包围盒+1范围</param>
    /// <param name="curveInfos"></param>
    static void IntersectWith(LineSegment3d xline, CurveInfo curveInfos)
    {
        // 求交点,得出交点参数
        _Cci3d.Set(xline, curveInfos, Vector3d.ZAxis);
        if (_Cci3d.NumberOfIntersectionPoints == 0)
            return;

        // 计算两条曲线的交点(多个),分别放入对应的交点参数集
        for (int i = 0; i < _Cci3d.NumberOfIntersectionPoints; i++)
        {
            var parArr = _Cci3d.GetIntersectionParameters(i);
            _parList.Clear();
            _parList.Add(parArr[0]);//0是第一条曲线的交点参数
            // 根据交点参数断分曲线
            var c3ds = curveInfos.GetSplitCurves(_parList);
            if (c3ds != null)
                for (int j = 0; j < c3ds.Count; j++)
                {
                    _plIns.Add(c3ds[j].EndPoint);
                    break;
                }
        }
    }


    /// <summary>
    /// 撞击边界
    /// </summary>
    public static void ImpactBoundary(List<CompositeCurve3d> scatteredEdges, DBTrans? tr = null)
    {
        if (scatteredEdges is null || scatteredEdges.Count == 0)
            throw new ArgumentNullException(nameof(scatteredEdges));

        tr ??= DBTrans.Top;

        // 曲线集合:炸开子段
        List<CurveInfo> curveInfos = new();
        scatteredEdges.ForEach(curve3d => {
            var ces = curve3d.Explode(new Interval(1e-6));
            ces.ForEach(a => curveInfos.Add(new(a)));
        });

        // 包围盒排序,按照包围盒中点排序,
        // 防止合并图元时候出现非顺序合并
        curveInfos = curveInfos.OrderBy(a => a._Y + (a.Height / 2))
                               .OrderBy(a => a._X + (a.Width / 2))
                               .ToList();

        // 水平线集合<y,断开关系>
        SortedDictionary<double, List<SuperPoly>> horizontal = new();
        // 垂直线集合<x,断开关系>
        SortedDictionary<double, List<SuperPoly>> vertical = new();

        // 步进数组,
        // 在分离填充边界的撞边是不需要线性步进(填充需要线性步进,同时也需要注意下面)
        // 0x01
        // 两个图元同端点时,横向扫描撞击的是同X,纵向扫描撞击会同Y,会造成奇数点数,
        // 所以不允许撞击端点,只能撞击图元腰部(包围盒腰部换算方便快速)
        // 同时,样条曲线包围盒不在撞击上,只能撞击腰部
        SortedSet<double> xSets = new();
        SortedSet<double> ySets = new();
        for (int i = curveInfos.Count - 1; i >= 0; i--)
        {
            var curve = curveInfos[i];
            if (curve.Height > CadTolerance.EqualPoint)// 0是水平线
                ySets.Add(Math.Abs(curve._Top - curve._Y) / 2 + curve._Y);
            else
                JoinPoly(horizontal, curve._Y, curve);// 连接碎线,加入水平(垂直)集合中

            if (curve.Width > CadTolerance.EqualPoint)// 0是垂直线
                xSets.Add(Math.Abs(curve._Right - curve._X) / 2 + curve._X);
            else
                JoinPoly(vertical, curve._X, curve);// 连接碎线,加入水平(垂直)集合中
        }

        if (xSets.Count == 0 || ySets.Count == 0)
        {
            Env.Printl("(xSets == 0 || ySets  == 0 ) 无效范围");
            return;
        }

        // 为了保证每个曲线仅加入一组
        Dictionary<CurveInfo, SoCuvers> groups = new();
        // 横向扫描线
        Scanlines yxScanlines = new();
        // 纵向扫描线(台字形,仅横向扫描的话,就有两个碰撞区,所以需要增加垂直扫描)
        Scanlines xyScanlines = new();

        LineSegment3d xline;
        foreach (var aset in ySets)
        {
            // 一条水平扫描线的交点
            Scanline scanline = new();
            for (int i = 0; i < curveInfos.Count; i++)
            {
                if (curveInfos[i].Height < CadTolerance.EqualPoint)
                    continue;

                // X扫描线在包围盒范围
                if (curveInfos[i]._Y <= aset && aset <= curveInfos[i]._Top)
                {
                    _plIns.Clear();
                    xline = new LineSegment3d(
                        new Point3d(curveInfos[i]._X - 1/*1是容差*/, aset, 0),
                        new Point3d(curveInfos[i]._Right + 1/*1是容差*/, aset, 0));

                    IntersectWith(xline, curveInfos[i]);
                    if (_plIns.Count != 0)
                    {
                        if (_plIns.Count == 1)
                        {
                            // 样条曲线 闭合打圈圈(捆扎带型),它的包围盒没有逼近,会在外部,
                            // 所以可能获取单个点,跳过此情景的加入,避免误认为是奇数点
                            if (curveInfos[i].IsCurve)
                                continue;
                        }
                        // 单一样条曲线封闭有多个交点,将交点和曲线记录
                        foreach (var pt in _plIns)
                        {
                            CreateOrGetGroup(curveInfos[i], groups);
                            scanline.Add(new IntersectGroup(pt.X, curveInfos[i]));
                        }
                    }
                }
            }

            // 因为是两两配对,所以它为偶数才是对的....偶数分析移动到后面做
            //if ((scanline.Count % 2) != 0)
            //{
            //    Env.Printl("边界错误");
            //    return;
            //}

            // 横向排序
            if (scanline.Count > 0)
            {
                scanline = scanline.OrderBy(ig => ig.XorY).ToList();
                yxScanlines.Add(scanline);
            }
        }

        foreach (var aset in xSets)
        {
            // 一条垂直扫描线的交点
            Scanline scanline = new();
            for (int i = 0; i < curveInfos.Count; i++)
            {
                if (curveInfos[i].Width < CadTolerance.EqualPoint)
                    continue;

                // X扫描线在包围盒范围
                if (curveInfos[i]._X <= aset && aset <= curveInfos[i]._Right)
                {
                    _plIns.Clear();
                    xline = new LineSegment3d(
                        new Point3d(aset, curveInfos[i]._Y - 1/*1是容差*/, 0),
                        new Point3d(aset, curveInfos[i]._Top + 1/*1是容差*/, 0));

                    IntersectWith(xline, curveInfos[i]);
                    if (_plIns.Count != 0)
                    {
                        if (_plIns.Count == 1)
                        {
                            // 样条曲线 闭合打圈圈(捆扎带型),它的包围盒没有逼近,会在外部,
                            // 所以可能获取单个点,跳过此情景的加入,避免误认为是奇数点
                            if (curveInfos[i].IsCurve)
                                continue;
                        }
                        // 单一样条曲线封闭有多个交点,将交点和曲线记录
                        foreach (var pt in _plIns)
                        {
                            CreateOrGetGroup(curveInfos[i], groups);
                            scanline.Add(new(pt.Y, curveInfos[i]));
                        }
                    }
                }
            }

            // 因为是两两配对,所以它为偶数才是对的
            //if ((scanline.Count % 2) != 0)
            //{
            //    Env.Printl("边界错误");
            //    return;
            //}

            // 垂向排序
            if (scanline.Count > 0)
            {
                scanline = scanline.OrderBy(ig => ig.XorY).ToList();
                xyScanlines.Add(scanline);
            }
        }

        // 画线,模拟填充,两两配对的
        {
            var arr = ySets.ToArray();
            var arrNum = 0;
            var aset = arr[arrNum];
            //APairTask(yxScanlines, (a, b) => {
            //    tr.CurrentSpace.AddLine(new(a.XorY, aset, 0), new(b.XorY, aset, 0));
            //}, () => {
            //    if (arrNum < arr.Length - 1)
            //        aset = arr[++arrNum];
            //});
            {
                // 已经排序了,从最下面起,遍历每行
                for (int i = 0; i < yxScanlines.Count; i++)// 行
                {
                    if (yxScanlines[i].Count % 2 == 0)// 当前点数是偶数,可能是两个独立的3点组成6点,因此需要处理
                    {
                        // [0]是否已经涂黑,有全部涂黑
                        if (yxScanlines[i][0].Color)
                        {
                            //for (int j = 0; j < yxScanlines[i].Count; j++)
                            //    yxScanlines[i][j].Color = true;//涂黑
                        }
                        else
                        {
                            // 如果没有,逆转判断
                        }
                    }
                    else
                    {
                        if (yxScanlines[i].Count == 1)
                            throw new ArgumentException("错误的情况 yxScanlines[i].Count == 1");

                        // 三个三个判断
                        for (int j = 2; j < yxScanlines[i].Count; j += 2)
                        {
                            var a = yxScanlines[i][j - 2].Color;
                            var b = yxScanlines[i][j - 1].Color;
                            var c = yxScanlines[i][j].Color;

                            if ((!a && !b && c) ||  //白,白,黑
                                (a && !b && !c)     //黑,白,白
                               )
                            {
                                yxScanlines[i][j - 1].Color = true;
                                continue;
                            }
                        }
                    }
                }
            }
        }

        {
            var arr = xSets.ToArray();
            var arrNum = 0;
            var aset = arr[arrNum];
            APairTask(xyScanlines, (a, b) => {
                tr.CurrentSpace.AddLine(new(aset, a.XorY, 0), new(aset, b.XorY, 0));
            }, () => {
                if (arrNum < arr.Length - 1)
                    aset = arr[++arrNum];
            });
        }

        /*
         * 获取分离填充的边界:
         * if (左组 != 右组) {
         *    总组.Remove(右组)
         *    总组.Add(左组)
         *    左组.AddRange(右组.Value)
         *    右组 = 左组
         * }
         * 最后 多少个组 就是多少个 填充边界集合
         *
         */

        // 碰撞区域:(碰撞区1,碰撞区2,碰撞区3...)
        // 炸开的口字型: 这里会为两个碰撞区域:(左右,上下)需要特别处理
        HashSet<SoCuvers> regions = new();
        APairTask(yxScanlines, (a, b) => {
            // 同一条样条曲线采样,那么左右都是它,跳过
            if (a.Curve == b.Curve)
            {
                regions.Add(groups[a.Curve]);
                return;
            }
            // 已经归类为同一组,跳过
            var lg = groups[a.Curve];
            var rg = groups[b.Curve];
            if (lg == rg)
                return;

            // 优化速度:把少的加过来
            if (lg.Count >= rg.Count)
            {
                regions.Remove(rg);
                regions.Add(lg);
                foreach (var item in rg)
                    lg.Add(item);
                groups[b.Curve].Clear();
                groups[b.Curve] = lg;
            }
            else
            {
                regions.Remove(lg);
                regions.Add(rg);
                foreach (var item in lg)
                    rg.Add(item);
                groups[a.Curve].Clear();
                groups[a.Curve] = rg;
            }
        });

        APairTask(xyScanlines, (a, b) => {
            // 同一条样条曲线采样,那么左右都是它,跳过
            if (a.Curve == b.Curve)
            {
                regions.Add(groups[a.Curve]);
                return;
            }
            // 已经归类为同一组,跳过
            var lg = groups[a.Curve];
            var rg = groups[b.Curve];
            if (lg == rg)
                return;

            // 优化速度:把少的加过来
            if (lg.Count >= rg.Count)
            {
                regions.Remove(rg);
                regions.Add(lg);
                foreach (var item in rg)
                    lg.Add(item);
                groups[b.Curve].Clear();
                groups[b.Curve] = lg;
            }
            else
            {
                regions.Remove(lg);
                regions.Add(rg);
                foreach (var item in lg)
                    rg.Add(item);
                groups[a.Curve].Clear();
                groups[a.Curve] = rg;
            }
        });


        var regionList = regions.ToList();
        RegionsRemoveHorizontalAndVertical(regionList, horizontal, vertical);

        // 填充边界结果
        List<SuperPoly> result = new();
        HashSet<CurveInfo> dealWith = new();
        // 会被改动的水平(垂直)区
        var horizontalArr = horizontal.ToList();
        var verticalArr = vertical.ToList();
        // 遍历碰撞区集合
        foreach (var regionItem in regionList)
        {
            // 已经处理的直接跳过
            if (dealWith.Contains(regionItem.First()))
                continue;

            // 每个碰撞区
            var region = regionItem.ToList();

            // 取末尾转为 超级多段线
            var hatchPoly = new SuperPoly(region[region.Count - 1]);
            region.RemoveAt(region.Count - 1);

            while (!hatchPoly.IsClosed)
            {
                // 循环剩余边界,如果加入成功,就继续加入,
                // 否则边缘可能是 水平(垂直)线区 上面
                for (int i = region.Count - 1; i >= 0; i--)
                {
                    var sat = hatchPoly.GetAddSatisfy(region[i]);
                    if (sat != SuperPoly.AddSatisfyModes.None)
                    {
                        // 成功一个,移除一个,并且重新开始
                        hatchPoly.Add(region[i], sat);
                        region.RemoveAt(i);
                        i = region.Count;// 循环尾还有--
                    }
                }
                if (hatchPoly.IsClosed)
                    break;

                // 在水平区域找
                LinkVH(horizontalArr, hatchPoly, region);
                if (hatchPoly.IsClosed)
                    break;

                // 在垂直区域找
                LinkVH(verticalArr, hatchPoly, region);
                if (hatchPoly.IsClosed)
                    break;
            }

            // 记录已经处理的
            for (int i = 0; i < hatchPoly.Sub.Length; i++)
                dealWith.Add(hatchPoly.Sub[i]);
            result.Add(hatchPoly);
        }

        Env.Printl("填充边界一共: " + result.Count + " 条");
        int groupNum = 1;
        foreach (var re in result)
        {
            var sb = new StringBuilder();
            sb.Append($"第 {groupNum++} 条: {re.Sub.Length} 个图元");
            sb.Append("\n{Curve: ");
            foreach (var curve in re.Sub)
                sb.Append($"{curve.GetHashCode()},");
            sb.Append('}');
            Env.Printl(sb.ToString());
        }
    }

    /// <summary>
    /// 碰撞区剔除仅有水平(垂直)的边界
    /// 否则 口口口 三个并排矩形会在后面无限循环(在两个离散图元永远不是连接的)
    /// </summary>
    /// <param name="regionList">碰撞区</param>
    /// <param name="horizontal">水平区</param>
    /// <param name="vertical">垂直区</param>
    static void RegionsRemoveHorizontalAndVertical(List<SoCuvers> regionList,
        SortedDictionary<double, List<SuperPoly>> horizontal,
        SortedDictionary<double, List<SuperPoly>> vertical)
    {
        for (int i = regionList.Count - 1; i >= 0; i--)
        {
            var region = regionList[i];// 碰撞区
            int count = region.Count;

            foreach (var regInfo in region)// 碰撞区的离散图元
                if (horizontal.ContainsKey(regInfo._Y))
                    QueryCountDown(horizontal.Values, regInfo, ref count);
            if (count < 1)
            {
                regionList.RemoveAt(i);
                continue;
            }

            foreach (var regInfo in region)// 碰撞区的离散图元
                if (vertical.ContainsKey(regInfo._X))
                    QueryCountDown(vertical.Values, regInfo, ref count);
            if (count < 1)
            {
                regionList.RemoveAt(i);
                continue;
            }
        }
    }

    /// <summary>
    /// 查询得到就计数-1
    /// </summary>
    /// <param name="listpolys">查询的表</param>
    /// <param name="regInfo">查询的值</param>
    /// <param name="count">计数</param>
    static void QueryCountDown(IEnumerable<List<SuperPoly>> listpolys, CurveInfo regInfo, ref int count)
    {
        foreach (var polys in listpolys)
        {
            foreach (var poly in polys)
            {
                if (poly.Contains(regInfo))
                    count--;
            }
        }
    }

    /// <summary>
    /// 碰撞区连接水平线或者垂直线
    /// </summary>
    /// <param name="vh">水平(垂直)集合</param>
    /// <param name="hatchPoly">填充边界</param>
    /// <param name="region">碰撞区</param>
    /// <returns></returns>
    static void LinkVH(List<KeyValuePair<double, List<SuperPoly>>> vh,
        SuperPoly hatchPoly, List<CurveInfo> region)
    {
        /*
         * 当前 poly 的边缘是 水平(垂直)线 上面
         * 水平(垂直)线 可能有多条一样Y轴的
         * 遍历 水平(垂直)线 ,提取一条出来判断是否能够加入.
         * 当这条 水平(垂直)线 满足 碰撞线1 的加入条件的时,还要满足 碰撞线2 的加入条件.
         * 也就是同时支持两个端点才加入
         * 那么就可以同时把 水平(垂直)线 以及 碰撞线2 加入.
         */
        bool flag;
        for (int vhNum = 0; vhNum < vh.Count; vhNum++)
        {
            flag = false;
            var vhPolys = vh[vhNum].Value;// 可连接的多个:同Y或者同X的断开的多个
            for (int vhPolysNum = vhPolys.Count - 1; vhPolysNum >= 0; vhPolysNum--)
            {
                var vbpl = vhPolys[vhPolysNum];
                var sat1 = hatchPoly.GetAddSatisfy(vbpl);
                if (sat1 == SuperPoly.AddSatisfyModes.None)
                    continue;

                // 遍历碰撞区,获取 碰撞线2 的曲线
                for (int regNum = region.Count - 1; regNum >= 0; regNum--)
                {
                    var curveinfo2 = region[regNum];
                    var sat2 = vbpl.GetAddSatisfy(curveinfo2);
                    if (sat2 != SuperPoly.AddSatisfyModes.None)
                    {
                        flag = true;
                        // 加入 水平(垂直)线
                        hatchPoly.Add(vbpl);
                        // 加入 碰撞线2
                        hatchPoly.Add(curveinfo2, sat2);
                        // 移除在 水平(垂直)区
                        vhPolys.RemoveAt(vhPolysNum);
                        // 移除碰撞区的 碰撞线2的曲线
                        region.RemoveAt(regNum);
                        // 碰撞区可能含有水平(垂直)区的,移除它们
                        for (int i = 0; i < vbpl.Sub.Length; i++)
                            region.Remove(vbpl.Sub[i]);
                        break;
                    }
                }
                if (flag)
                    break;
            }
            if (vhPolys.Count == 0)
                vh.RemoveAt(vhNum);
            if (flag)
                break;
        }
    }

    /// <summary>
    /// 连接水平(垂直)
    /// </summary>
    /// <param name="vh"></param>
    /// <param name="keyXy"></param>
    /// <param name="valueCurveInfo"></param>
    static void JoinPoly(SortedDictionary<double, List<SuperPoly>> vh,
                         double keyXy,
                         CurveInfo valueCurveInfo)
    {
        if (vh.ContainsKey(keyXy))
        {
            // 将同水平(垂直)线进行连接,如果连接不成功,表示它们是断开的关系
            bool flag = false;
            var polys = vh[keyXy];
            for (int n = 0; n < polys.Count; n++)
            {
                var addsat = polys[n].GetAddSatisfy(valueCurveInfo);
                if (addsat != SuperPoly.AddSatisfyModes.None)
                {
                    // 不是断开关系就能加入其中一个
                    polys[n].Add(valueCurveInfo, addsat);
                    flag = true;
                    break;
                }
            }
            if (!flag)
                polys.Add(new SuperPoly(valueCurveInfo));
        }
        else
        {
            vh.Add(keyXy, new List<SuperPoly>() { new SuperPoly(valueCurveInfo) });
        }
    }

    /// <summary>
    /// 左右两个碰撞点为一组输出
    /// </summary>
    /// <param name="yxlines">所有行</param>
    /// <param name="aPairAction">左右两个(点,图元)为一组输出</param>
    /// <param name="lineAction">每次线切换的任务,用于增加步进距离</param>
    static void APairTask(Scanlines yxlines,
                          Action<IntersectGroup, IntersectGroup> aPairAction,
                          Action? lineAction = null)
    {
        // 已经排序了,从最下面起,遍历每行
        for (int i = 0; i < yxlines.Count; i++)
        {
            // 两个为一组,画线
            for (int j = 1; j < yxlines[i].Count; j += 2)
                aPairAction.Invoke(yxlines[i][j - 1], yxlines[i][j]);
            lineAction?.Invoke();
        }
    }

    /// <summary>
    /// 获取所在组
    /// </summary>
    /// <param name="info"></param>
    /// <returns></returns>
    static SoCuvers CreateOrGetGroup(CurveInfo info, Dictionary<CurveInfo, SoCuvers> groups)
    {
        SoCuvers? gr = null;
        if (groups.ContainsKey(info))
            gr = groups[info];

        if (gr == null)
        {
            gr = new() { { info } };
            groups.Add(info, gr);
        }
        return gr;
    }
}


/// <summary>
/// 扫描线交点上面的图元
/// </summary>
public class IntersectGroup
{
    public double XorY;
    public CurveInfo Curve;
    public bool Color;
    public IntersectGroup(double xOrY, CurveInfo group)
    {
        XorY = xOrY;
        Curve = group;
        Color = false;
    }
}


/// <summary>
/// 临时创建的图元集合,求解完之后用于释放
/// </summary>
public class DisEntitys : List<Entity>, IDisposable
{
    #region IDisposable接口相关函数
    public bool IsDisposed { get; private set; } = false;

    /// <summary>
    /// 手动调用释放
    /// </summary>
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    /// <summary>
    /// 析构函数调用释放
    /// </summary>
    ~DisEntitys()
    {
        Dispose(false);
    }

    protected virtual void Dispose(bool disposing)
    {
        // 不重复释放
        if (IsDisposed) return;
        IsDisposed = true;

        for (int i = 0; i < this.Count; i++)
        {
            if (this[i].IsDisposed)
                continue;
            if (this[i].Database == null)
                this[i].Dispose();
        }
    }
    #endregion
}

(完)

posted @ 2020-03-25 23:25  惊惊  阅读(4200)  评论(2编辑  收藏  举报