cad.net bo边界算法
发送lisp
(bpoly point)
发送命令
Acad的低版本只能发送命令的方式获取边界信息,edata说这个bo是写在arx内的,不是接口,所以也不能反射用(没深究了)
低版本:
通过命令发送bo的方法:发送bo命令例子
要注意的是,如果空格再次执行上次命令,你会执行到bo,而不是你的当前命令.
而正常的cad用户都是要执行当前的命令.
例如填充命令发送bo命令,那么再次空格是bo,而用户需要是填充
解决方法:要解开文档锁的情况下发送一次当前命令,而且在命令函数外做一个立即结束的标记,
在最前面写:
到了Acad2011: 就可以通过以下语句获取
Editor ed = Acap.DocumentManager.MdiActiveDocument.Editor;
ed.TraceBoundary(........);
算法概念
但是,要是自己能造一个边界算法,为什么要靠桌子的.
打倒自动桌子!!!! 打倒Acad!!!
分析桌子的bo算法
桌子的bo算法原理:
先获取当前屏幕可视的图形,然后用的是像素一点一点抓边界像素(填充法),所以非常快.
知道了之后,就明白这是一个前台操作,而我们需要挑战的是后台bo算法,也就是无像素的前提下分析边界.
而且必须要做得很快,不然就没有意义啦.
方案一,单点射线法(实际上就是夹角左转法)
于是乎,我做了一张图(拖拉图片看大图)
不要问为什么是奇点不是起点,因为起点的意义太多了,换个字免得引起歧义而已(用原点又会跟世界坐标原点重意辣!像爆炸一样辐射获取收到冲击波的边界).
1, 减少遍历,不可能O(n²)比较的,所以我们利用四叉树
制作一个查找临近图元的功能,这个比较简单,我都写完了.
2, 抛弃非边界计算图元,例如文字,标注,引线.
如果奇点,不在块的嵌套层次上,那么表示块边界才是遍历的,(此边界相当于bv树的外层)
如果在嵌套层次里,先算层次内,边界没密封才继续遍历其他块边界.
下面是山人告诉我的bo算法思想... 它就是图上的拆解算法.
样图
全部交点打断,然后bo到的边界就是我标数字的线段组成的.(其实不是打断,是求交点,我是便于大家理解)
步骤如下
0x01 射线求交点
从A点做0°直线(射线),与所有的线只有4个交点,就是我画绿色圆的点;
选择一个离A点最近的点,并通过这个点得到1号直线: 数学上来讲就是两个直线方程联立.
0x02 获取下一段线段
得到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号线,循环结束;
分析
经过0x01和0x02的步骤可知,每次都需要轮询一次所有的图元,那么这个轮询就是数量就是线性递减的,因为已经分析过的图元,除了第一个需要保留以判断闭环之外,其他都可以抛弃.
这个是最简单的,没有内部孤岛,没有曲线,的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
}
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
}
(完)