cad.net 扫描线代码
原有链接
1,扫描线算法
https://www.cnblogs.com/JJBox/p/12571436.html
2,扫描线代码
https://www.cnblogs.com/JJBox/p/18652906.html
3,扫描线辅助类
https://www.cnblogs.com/JJBox/p/18677524.html
代码步骤
第一阶段:
1,串行读取全部图元.
2,放入块区,用于并行计算.
3,每个块区
3.1 数据清洗:炸开+每段单元化.
3.2 自交打断
捆扎带类型就可以剩下一个圈圈了,尾巴就打断了,之后可以剔除了.
3.3 碰撞打断
每个曲线交点都打断为更碎的线,每条线都是孤立的.
没有例外,包括闭合曲线.
直断线: 横向直断线/竖向直断线/斜边直断线.
求交时候打断成为多段直线!!
只会存在完全重叠,不会存在部分重叠.
这给我们一个很大的关注点分离性质.
4 构建邻接表
剪枝,剔除孤立的链条.捆扎带的尾巴.
我觉得它性能不好,感觉可以不要,
先想象没有任何尾巴,继续下去,学会关注点分离.
5,组是对边碰撞
对边必然是封闭区,
每个曲线信息都是一个组,然后我们只需要将它合并.
合并方式
a:交点重叠不能成组
虽然存在同交点重叠图元,但是它们不是组,否则破坏对边成组关系.
但是压根不存在扫描线碰撞到端点:
a1,如果曲线中点有另一条曲线端点压在图元上面,
会在数据清洗时候,判断为碰撞,进行打断成更碎,成为三条曲线.
a2,如果不是碰撞,就会+=微量,避开端点.
b:横扫和竖扫,间隔输出两点,把对边组合并.
5.1 斜边(纯水平,纯垂直之外的,虽然是曲线)
经过横扫和竖扫后,还会把某些纯水平线和纯垂直线被归类入斜线组.
组内比较拐点然后连接.(准确来说都是端点,不存在拐点,因为都炸碎啦)
纯水平垂直通过组数量,排除掉已经加入组.
所以斜边是不存在问题的,只需要组内比较连接.
组内连接存在非常多问题,我们最下面讲.
第二阶段:
维护索引的一系列事件.
1,每个投影区都要缓存,用来添加图元之后立即计算面域.
2,面域(环形)需要缓存起来作为索引.
3,跨文档的缓存用static字段: map[doc,缓存]
命令
using MyGroup = System.Collections.Generic.HashSet<CurveInfo>;
/* 第二阶段才去做
public partial class TestScanlineCommands {
[CommandMethod(nameof(TestJTopo))]
public void TestJTopo() {
Tolerance oldtol = Tolerance.Global; // 旧容差
Tolerance.Global = new Tolerance(1e-6, 1e-6);
// 遍历全图进行分区...
RangeMap<Entity> map = new();
foreach(var item in 分区表) {
var topo = new JTopo();
topo.GetEdges(item);
}
Tolerance.Global = oldtol;
}
}
*/
public partial class TestScanlineCommands {
[CommandMethod(nameof(TestBo))]
public void TestBo() {
var ss = Env.Editor.SSGet();
if (ss is null) return;
using DBTrans tr = new();
var ents = ss.Value.GetEntities<Curve>()
.Select(e => {
if (e is null) return null;
return new CurveInfo(e); // 辅助类搬运到 https://www.cnblogs.com/JJBox/p/18677524
})
.Where(gc => gc is not null)
.OrderBy(gc => gc.Y)
.ThenBy(gc => gc.X)
.ToList();
if (ents is null) return;
ss = null;
if (ents.Count < 20) {
// 串行遍历
GetIntersectionParameters(ents, 0);
}
else {
// 并行遍历,碰撞求曲线参数
int coreCount = Environment.ProcessorCount;
int chunkSize = (ents.Length + coreCount - 1) / coreCount;
var tasks = Enumerable.Range(0, coreCount)
.AsParallel()
.Select(i => Task.Run(() => GetIntersectionParameters(ents, i * chunkSize)))
.ToList();
Task.WaitAll(tasks);
}
// 数据清洗
// 孤岛: 闭合&&无碰撞
// 闭合可能和其他碰撞,要判断曲线参数数量为碰撞
// 捆扎带型会被自交求解为闭合类型?
Task<List<CurveInfo>> getClosedTask = Task.Run(() => {
return ents.AsParallel()
.Where(gc => gc.Paramss.Count == 0 && gc.OriginalC3d.IsClosed())
.ToList();
});
Task<List<CurveInfo>> getLinkTask = Task.Run(() => {
// 复合曲线:曲线参数断分而来
// 制作邻接表只需要断分,炸开反而增加了邻接表大小
var list = ents.AsParallel()
.Where(gc => gc.Paramss.Count > 0)
.SelectMany(gc => gc.BreakCurves())
.ToList();
// 邻接表,剔除链尾
// 这里1.00和0.99就不一样了,让用户自己提前处理
// var sp = info.OriginalC3d.StartPoint.GetHashString(2);
// var ep = info.OriginalC3d.EndPoint.GetHashString(2);
// 内部持有list指针弱引用,避免覆盖操作之后不能回收,骚得很
AdjacencyList<Point3d, CurveInfo> adja = new(list)
.SetStartKey(info => info.OriginalC3d.StartPoint)
.SetEndKey(info => info.OriginalC3d.EndPoint);
var map = adja.AsParallel().ToMap(); // 暴露才能打印看看
adja.PrunBranch(map);
return list;
});
Task.WaitAll(getClosedTask, getLinkTask);
ents = null;
// 曲线集合:自闭
var curvesClosed = getClosedTask.Result;
// 曲线集合:不自闭,和其他形成闭合范围
var curvesLink = getLinkTask.Result;
// 单元曲线,炸开而来
// 扫描线需要单元曲线,不然∧曲线无断分造成提取端点错误.
curvesLink = curvesLink.AddRange(curvesClosed)
.AsParallel()
.SelectMany(gc => gc.ExplodeToInfo())
.ToList();
// 此处加入小试牛刀
// 此处加入构建超级边界树
var sh = new ScanlineHelper();
// sh.SimulateHatch(curvesLink);
// sh.DrawLines();
// sh.Clear();
sh.ImpactBoundary(curvesLink);
sh.GetAllSuperPoly();
sh.DrawLines();
sh.Clear();
}
// 获取曲线参数
// 这里其实是可以并行的,见案例7
void GetIntersectionParameters(List<CurveInfo> ents, int start) {
if (start < 0 || start >= ents.Count)
throw new ArgumentException("错误的索引位置");
using CurveCurveIntersector3d cci3d = new();
for (int i = start; i < ents.Count; i++) {
var gc1 = ents[i];
var int1 = gc1.OriginalC3d.GetInterval(); // 曲线的参数区间
// 第一次是自交比较,捆扎带类型将被打断
for (int j = i; j < ents.Count; j++) {
var gc2 = ents[j];
// 已经排序,超过范围之后都不会成功
if (gc2.Y > gc1.Top) break;
// 包围盒没有碰撞相交
if (!gc1.IntersectsWith(gc2)) continue;
// 求交
cci3d.Set(gc1.OriginalC3d, gc2.OriginalC3d,
int1, gc2.OriginalC3d.GetInterval(), Vector3d.ZAxis);
/*
var a = cci3d.GetIntersectionRanges(); // 相交范围
var d = cci3d.OverlapCount(); // 重叠区域数量
for (int m = 0; m < d; m++) {
// 重叠范围,返回类型是区间数组
// 通过它应该可以直接消重
Interval[] b = cci3d.GetOverlapRanges(m);
}
*/
for (int k = 0; k < cci3d.NumberOfIntersectionPoints; k++) {
// var pt = cci3d.GetIntersectionPoint(k); // 交点坐标
var pars = cci3d.GetIntersectionParameters(k);
gc1.Paramss.Add(pars[0]); // 第一条曲线的交点参数
gc2.Paramss.Add(pars[1]); // 第二条曲线的交点参数
}
}
}
}
}
小试牛刀
public partial class ScanlineHelper {
/// <summary>
/// 模拟填充
/// </summary>
/// <param name="curveInfos">炸开的图元集合</param>
public void SimulateHatch(List<CurveInfo> curveInfos) {
// 曲线集合,包围盒排序
curveInfos = curveInfos
.OrderBy(a => a.Y)
.ThenBy(a => a.X)
.ToList();
// 获取所有包围盒的最上最下
double maxY = curveInfos.Aggregate(double.MinValue, (max, info) => Math.Max(max, info.Top));
double minY = curveInfos.Aggregate(double.MaxValue, (min, info) => Math.Min(min, info.Y));
if (maxY - minY <= 0) return;
// 线性步进
var det = (maxY - minY) / 1000;
if (det <= 0) return;
minY += det; // 跳过最下面
List<double> values = new();
for (var aset = minY; aset < maxY; aset += det) {
Scanline<CurveInfo> scanline = new(aset);
// 你会看见这个无可避免遍历全图
// 正式操作时候用了二分法,但是有代价
for (int i = 0; i < curveInfos.Count; i++) {
var info = curveInfos[i];
// 已经排序,超过范围之后都不会成功
if (info.Y > aset) break;
// 扫描线不在包围盒范围跳过
if (!(info.Y <= aset && aset <= info.Top)) continue;
GetX(aset, info, values);
// 样条曲线封闭有多个交点
for (int x = 0; x < values.Count; x++)
scanline.Add(values[x], info);
if (values.Count > 0) values.Clear();
}
if (scanline.Count == 0) continue;
if ((scanline.Count % 2) != 0) throw new("边界错误不是偶数");
_hScanlines.Add(scanline);
}
}
// 画线,模拟填充,两两配对的
public void DrawLines() {
foreach(var line in _hScanlines) {
line.APairTask((left, right) => {
tr.CurrentSpace.AddLine(
new Point3d(left.Key, line.Num, 0),
new Point3d(right.Key, line.Num, 0));
});
}
foreach(var line in _vScanlines) {
line.APairTask((left, right) => {
tr.CurrentSpace.AddLine(
new Point3d(line.Num, left.Key, 0),
new Point3d(line.Num, right.Key, 0));
});
}
}
public void Clear() {
_hScanlines.Clear();
_vScanlines.Clear();
_horizontal.Clear();
_vertical.Clear();
}
}
通过交点分类边界
上面小试牛刀之后我们知道了线性步进非常耗时,
填充才需要线性步进,下面是中点步进.
下面开始正式操作:
重点的四个字段集合,
因为横向扫描线无法击中水平线,所以独立集合.
纯水平或垂直之外的我将它们纳入斜线区,虽然它是曲线.
1,扫描线是击中腰部(中点).
并且避开端点,这样减少大量比较,不需要线性步进.
两个图元同端点时,
横向扫描撞击的是同X,竖向扫描撞击会同Y,会造成奇数点数,
所以只能撞击图元腰部(包围盒腰部换算快速)
同时,样条曲线图元不在包围盒边缘,只能撞击腰部.
2,为什么需要两个方向扫描线?
仅横向扫描的话,台字形,就有两个碰撞区,无法连接.
原因是斜组和斜组不能通过拐点相连,会破坏对边成组规定.
斜组只能和横区/竖区拐点相连.
3,超级多段线就是最终边界.
斜组通过端点连接可以解决大部分情况.
但是炸开是口字,需要跨区域再用端点进行判断和连接.
基础的条件就是这样.
public partial class ScanlineHelper {
// 1,横区<y, 断开关系>
SortedList<double, MyGroup> _horizontal = new();
// 2,竖区<x, 断开关系>
SortedList<double, MyGroup> _vertical = new();
// 3,横向扫描线,击中斜线和竖向的中心
SortedSet<Scanline<CurveInfo>> _hScanlines = new();
// 4,竖向扫描线,击中斜线和横向的中心
SortedSet<Scanline<CurveInfo>> _vScanlines = new();
/// <summary>
/// 获取边界
/// </summary>
/// <param name="curveInfos">炸开的图元集合</param>
public void ImpactBoundary(List<CurveInfo> curveInfos) {
// 下面都是基于任务并行,任务并行内是可以数据并行的,
// 但是我没有测试速度如何,先写成传统的,避免不必要的过度并行.
// 横区
Task hlineTask = Task.Run(() => {
var infos = curveInfos.Where(info => info.Height <= Tolerance.Global.EqualPoint);
foreach(var info in infos) {
info.RegionColor = 1;
if (!_horizontal.TryGetValue(info.Y, out var set)) {
set = new();
_horizontal.Add(info.Y, set);
}
set.Add(info);
}
});
// 竖区
Task vlineTask = Task.Run(() => {
var infos = curveInfos.Where(info => info.Width <= Tolerance.Global.EqualPoint);
foreach(var info in infos) {
info.RegionColor = 2;
if (!_vertical.TryGetValue(info.X, out var set)) {
set = new();
_vertical.Add(info.X, set);
}
set.Add(info);
}
});
// 中点步进数组,用来扫描,
// 此时虽然是求了中点,但也可能命中其他端点,
// 需要偏移+=微量,变相剔除端点.
// 毕竟可能是唯一扫描线,不能简单剔除.
// 之前用了加1e-6可能突出包围盒.改用(中点+边缘)/2,控制一定在内部.
// 但是样条曲线包围盒内部也可能没有击中.
// 是不完备的,只是我经过数据清洗之后能够排除
Task midYTask = Task.Run(() => {
// 端点数组y.
var ptset = new HashSet<double>(curveInfos.Count*1.75);
curveInfos.ForEach(info => {
ptset.Add(info.StartPoint.Y);
ptset.Add(info.EndPoint.Y);
});
// 扫描线排除端点冲突
var mset = new SortedSet<double>(curveInfos.Count*0.75);
foreach (var info in curveInfos) {
if (info.Height <= Tolerance.Global.EqualPoint) continue;
var t = info.MidY;
while (ptset.Contains(t)) t += (t - info.Y) / 2;
mset.Add(t);
}
ptset = null;
HScanlines(mset.ToArray(), curveInfos);
});
Task midXTask = Task.Run(() => {
// 端点数组x.
var ptset = new HashSet<double>(curveInfos.Count*1.75);
curveInfos.ForEach(info => {
ptset.Add(info.StartPoint.X);
ptset.Add(info.EndPoint.X);
});
// 扫描线排除端点冲突
var mset = new SortedSet<double>(curveInfos.Count*0.75);
foreach (var info in curveInfos) {
if (info.Width <= Tolerance.Global.EqualPoint) continue;
var t = info.MidX;
while (ptset.Contains(t)) t += (t - info.X) / 2;
mset.Add(t);
}
ptset = null;
VScanlines(mset.ToArray(), curveInfos);
});
Task.WaitAll(hlineTask, vlineTask, midYTask, midXTask);
}
// TODO 鱼和熊掌
// 因为要并行两个方向的扫描线,
// 而二分法需要排序,所以要创建副本,
// 如果单个分区10万个图元有30万个...并且有过程集合...
// 1,尽可能分区,但是分区是依照投影确定的,自适应.
// 2,不用二分法,直接搜,反正内部有包围盒判断,但是缺乏中断机制,每一条扫描线都要遍历全部数据.
// 3,串行执行,这样排序都用同一个数组,也可以有二分法.
// 横向扫描线
// 传入的ySets和图元排除水平的,而垂直的会进入来被击中.
// 之后才去垂直区剔除已经加入组的.
void HScanlines(double[] ySets, List<CurveInfo> curveInfos) {
if (ySets.Length == 0) throw new("无效范围");
// 中点排序
// 再利用前后两根扫描线夹选图元,就是二分法区间获取.
var yCurveInfos = curveInfos
.OrderBy(info => info.MidY)
.ThenBy(info => info.MidX)
.ToArray();
List<double> values = new();
var array = ySets;
for (int i = 0; i < array.Length; i++) {
// 热路径
var aset = array[i];
Scanline<CurveInfo> scanline = new(aset);
var infos = BinarySearch(yCurveInfos, info => info.MidY, array, i);
foreach (var info in infos) {
// 已经排序,超过范围之后都不会成功,
// 虽然是中点排序,但是用底边判断才是对的.前后夹选还是有一些超范围.
if (info.Y > aset) break;
// 跳过包围盒以外
if (!(info.Y <= aset && aset <= info.Top)) continue;
// 跳过水平撞水平
if (info.Height <= Tolerance.Global.EqualPoint) continue;
GetX(aset, info, values);
for (int i = 0; i < values.Count; i++) {
scanline.Add(values[i], info);
}
if (values.Count > 0) values.Clear();
}
if (scanline.Count == 0) continue;
if ((scanline.Count % 2) != 0) throw new Exception("边界错误不是偶数");
_hScanlines.Add(scanline);
}
}
// 竖向扫描线
void VScanlines(double[] xSets, List<CurveInfo> curveInfos) {
if (xSets.Length == 0) throw new("无效范围");
// 中点排序
// 再利用前后两根扫描线夹选图元,就是二分法区间获取.
var xCurveInfos = curveInfos
.OrderBy(info => info.MidX);
.ThenBy(info => info.MidY);
.ToArray();
List<double> values = new();
var array = xSets;
for (int i = 0; i < array.Length; i++) {
// 热路径
var aset = array[i];
Scanline<CurveInfo> scanline = new(aset);
var infos = BinarySearch(xCurveInfos, info => info.MidX, array, i);
foreach (var info in infos) {
// 已经排序,超过范围之后都不会成功,
// 虽然是中点排序,但是用底边判断才是对的.前后夹选还是有一些超范围.
if (info.X > aset) break;
// 跳过包围盒以外
if(!(info.X <= aset && aset <= info.Right)) continue;
// 跳过垂直撞垂直
if (info.Width <= Tolerance.Global.EqualPoint) continue;
GetY(aset, info, values);
for (int i = 0; i < values.Count; i++) {
scanline.Add(values[i], info);
}
if (values.Count > 0) values.Clear();
}
if (scanline.Count == 0) continue;
if ((scanline.Count % 2) != 0) throw new Exception("边界错误不是偶数");
_vScanlines.Add(scanline);
}
}
// 二分法
// 利用前后两根扫描线夹选图元,就是二分法区间获取.
IEnumerable<T1> BinarySearch<T1, T2>(T1[] curveInfos,
Func<T1, T2> selector, T2[] array, int arrayIndex) {
if (curveInfos is null)
throw new ArgumentNullException(nameof(curveInfos));
if (curveInfos.Length == 0) yield break;
int startIndex = 0;
int endIndex = curveInfos.Length - 1;
if (arrayIndex > 0)
startIndex = GetLeft(curveInfos, array[arrayIndex - 1], selector);
if (arrayIndex < array.Length - 1)
endIndex = GetRight(curveInfos, array[arrayIndex + 1], selector);
if (startIndex > endIndex)
yield break;
for (int i = startIndex; i <= endIndex; i++)
yield return curveInfos[i];
}
int GetLeft<T1, T2>(T1[] curveInfos, T2 target, Func<T1, T2> selector) where T2 : IComparable<T2> {
if (curveInfos is null) throw new ArgumentException("curveInfos is null");
if (curveInfos.Count == 0) return 0;
int left = 0, right = curveInfos.Length - 1, result = -1;
while (left <= right) {
int mid = left + (right - left) / 2;
int comparison = selector(curveInfos[mid]).CompareTo(target);
if (comparison >= 0) {
result = mid;
right = mid - 1;
}
else {
left = mid + 1;
}
}
// 优化后的重复元素查找,每次移动两步
while (result > 1 && selector(curveInfos[result - 1]).CompareTo(key) == 0) {
result -= 2;
if (result < 0 || selector(curveInfos[result]).CompareTo(key) != 0) {
result += 2;
break;
}
}
// 重复元素需要线性查找,第一个等于,找不到就是第一个大于
while (result > 0 && selector(curveInfos[result - 1]).CompareTo(target) == 0) {
result--;
}
// 不会返回-1的,最起码也是0,用于插入.
return result;
}
int GetRight<T1, T2>(T1[] curveInfos, T2 target, Func<T1, T2> selector) where T2 : IComparable<T2> {
if (curveInfos is null) throw new ArgumentException("curveInfos is null");
if (curveInfos.Count == 0) return 0;
int left = 0, right = curveInfos.Length - 1, result = -1;
while (left <= right) {
int mid = left + (right - left) / 2;
int comparison = selector(curveInfos[mid]).CompareTo(target);
if (comparison <= 0) {
left = mid + 1;
}
else {
result = mid;
right = mid - 1;
}
}
// 优化后的重复元素查找,每次移动两步
while (result < curveInfos.Length - 1 && selector(curveInfos[result]).CompareTo(key) == 0) {
result += 2;
if (result >= curveInfos.Length || selector(curveInfos[result]).CompareTo(key) != 0) {
result -= 2;
break;
}
}
// 重复元素需要线性查找,第一个大于
while (result < curveInfos.Length && selector(curveInfos[result]).CompareTo(target) == 0) {
result++;
}
// 不会返回-1的,最起码也是0,用于插入.
return result;
}
// 数据清洗
// info.IsCurve样条曲线 闭合打圈圈(捆扎带型),它的包围盒没有逼近,会在外部,
// 所以可能获取单个点,跳过此情景的加入,避免误认为是奇数点
// 线性步进的可以根据密度进行击中,但是这里是唯一扫描,
// 不过我击中是腰部,大概率会跳过
void GetX(double aset, CurveInfo info, List<double> result){
// 扫描线在包围盒范围,加一减一是突出包围盒
using var xline = new LineSegment3d(
new Point3d(info.X - 1, aset, 0),
new Point3d(info.Right + 1, aset, 0));
IntersectWith(xline, info.OriginalC3d, xList : result);
if (result.Count == 1 && info.IsCurve) result.Clear();
}
void GetY(double aset, CurveInfo info, List<double> result){
using var xline = new LineSegment3d(
new Point3d(aset, info.Y - 1, 0),
new Point3d(aset, info.Top + 1, 0));
IntersectWith(xline, info.OriginalC3d, yList : result);
if (result.Count == 1 && info.IsCurve) result.Clear();
}
/// <summary>
/// 求交点
/// </summary>
/// <param name="xline">扫描线,要求图元包围盒+1突出范围</param>
/// <param name="curve3d">曲线</param>
/// <param name="xList">返回x</param>
/// <param name="yList">返回y</param>
/// <param name="zList">返回z</param>
void IntersectWith(LineSegment3d xline, Curve3d curve3d,
List<double>? xList = null,
List<double>? yList = null,
List<double>? zList = null) {
// 求交类(每次set自动重置,都会有个新的结果),得出交点参数
using CurveCurveIntersector3d cci3d = new();
cci3d.Set(xline, curve3d, xline.GetInterval(), curve3d.GetInterval(), Vector3d.ZAxis);
// 计算两条曲线的交点(多个),分别放入对应的交点参数集
for (int k = 0; k < cci3d.NumberOfIntersectionPoints; k++) {
var pt = cci3d.GetIntersectionPoint(k);
xList?.Add(pt.X);
yList?.Add(pt.Y);
zList?.Add(pt.Z);
}
}
}
构造闭合边界
0x8A 并行每个组
如果不并行每个组,
那么就应该加入一个移除一个,
横区和竖区的value为0还得删key.
如果每个组内并行,
若加入一个剔除一个,那么VH容器将无法并行,
所以需要并行不移除VH,用颜色进行记录.
通过颜色来排除重复包含.
生成树期间,区是冻结的只是查询,组是会动态减少的.
那么并行每个组是否有并发?
组a=横组,组b=竖组,
他们需要根据拐点连接,但是遍历的时候刚刚好
task1,发生:
查询竖区,获取到组b成员,然后CAS改色.
组a.Add(组b成员)
task2,发生
查询横区,获取到组a成员,然后CAS改色.
组b.Add(组a成员)
组转超级多段线的时候会改颜色,
组a第一根,组b就搜不到(此时组b肯定不是环了),然后搜索组a其他成员改颜色成功.
组a完蛋组内优先被打破,组成员颜色被改后认定为已经加入组
同样,组b是并行的,也完蛋了
1,组内排除竖区,√
这样就不存在相互查找:横组找竖区,竖组找横区.
只会存在横找竖成员了.炸开的口也就能找到了.
2,如果不改颜色,或者不过滤颜色,但是这不应该.
3,如果构造超级多段线之前锁定某个标记,搜索是区不是组.
锁区,代价太大了吧.
横组成功获取锁,连接竖区成员.
竖组找到时候存在锁,
a放弃,然后查找其他,失败,不成环.
b自旋,查找的颜色过滤掉了,失败,不成环.
public partial class ScanlineHelper {
public void GetAllSuperPoly() {
// 0x51,交点重叠不能是同一个组
// 0x52,合并对边到同一个组√
// 此处仅奇数边界,偶数边界还没有做,主要是我有改颜色这种数据操作
// 遍历扫描线把全部组(包括斜边/横向/竖向)记录下来.
HashSet<MyGroup> groupSet = new();
MergeTwoPickGroup(groupSet, _hScanlines);
MergeTwoPickGroup(groupSet, _vScanlines);
// 创建闭合边界
// 一个组内只有一棵树,并行的是组,所以树没有跨线程.
var boTrees = groupSet.AsParallel()
.Select(group => {
// 0x8A 排除竖组纯色组,避免横竖相互查找
var has = group.FirstOrDefault(a => a.RegionColor != 2);
if (has is null) return null;
// 返回多叉树.
var boTree = new SuperTree<SuperPoly>();
FindInnerAndOuterRings(group, boTree);
return boTree;
}).ToList();
// 打印边界信息
Print(boTrees); // TODO 这里还没改好
}
void MergeTwoPickGroup(HashSet<MyGroup> groupSet,
SortedSet<Scanline<CurveInfo>> scanlines){
foreach(var line in scanlines) {
line.APairTask((left, right) => {
// 输出对边
// 同一组加入其中一个
var g1 = left.Value;
var g2 = right.Value;
if (g1 == g2) {
groupSet.Add(g1);
return;
}
// 优化速度:把少的加过来
if (g1.Count < g2.Count) (g1, g2) = (g2, g1);
groupSet.Remove(g2);
groupSet.Add(g1);
g1.UnionWith(g2);
g2.ForEach(info => info.Group = g1);
g2.Clear();
});
}
}
// 打印信息
public void Print(List<SuperPoly> superPolys){
var sb = new StringBuilder();
sb.Append($"填充边界一共: {superPolys.Count} 条");
int groupNum = 1;
foreach (var superPoly in superPolys) {
sb.Append($"\n第 {groupNum++} 条: {superPoly.Sub.Length} 个图元");
sb.Append("\n{Curve: ");
foreach (var curve in superPoly.Sub)
sb.Append($"{curve.GetHashCode()},");
sb.Append('}');
}
Env.Printl(sb.ToString());
}
}
思路
查找封闭区问题
A:每一个组肯定存在封闭,因为不封闭的已经剪枝了.
B:创建闭合边界时,先在同组内连接,当无法完成时候,去其他组查找.
那么其他组中,
只可能是_horizontal(横区)和_vertical(竖区)内的,
不可能是其他斜边组,因为会破坏对边成组原则.
横区和竖区:纯水平垂直
剩下某些纯水平和纯垂直,炸开的口字,它们需要拐点连接.
口是单节或者多节的:
------
| |
| |
------
C:多重封闭区: 花瓣型/孤岛碰撞.
共点的花瓣:
(())双数花瓣型,只需要对边成组,会被分为两组.其后分别通过拐点相融就可以组合一起了.
())奇数花瓣型,会对边成组时候剔除.
孤岛碰撞型,虽然对边成组,但是仍然存在横竖区的查找问题.
(下方还有图形介绍)
填充边界生成过程:
染色方案:
因为存在孤岛碰撞问题
第一阶段:
判断能否加入链中,需要排除非0,1,2三种基础色的.
第二阶段:
从链成为环,会加多了对象,解环并剥离.
为了并行,横区竖区不能获取一个移除一个,
需要将加入的对象需要标记新颜色,才能在避免重复加入.
(或线程安全hashset记录边界,已经放弃)
我还怀疑可以邻接表检索.
第三阶段:
把内边界加入树,
循环组内对象回到第一阶段.
下面的重点就是同一个组内如何分析
图形案例1,2,3,4
下面案例1,2,3,4只需要注意,
当找到一条横线或者竖线,就得返回组内查找并加入.
而不是横线之后去竖线查找.
案例1:
L ̄\7
如果先找到后面的7是错误的.
因为7也存在有横线记录在横区.但是现在横线排序的,是不存在此问题.
从下到上,从左到右.
案例2
_____
| /__|
0,左竖线加入超级多段线.
1,组内连接成功|/
左下角腰点已经连接排除掉,端点进行tryadd
2,横区已经排序,
底部横线优先级最高,连接端点失败.
3,垂直失败.
4,顶部横线成功.
5,闭环
案例3
_____
|_/__|
0,左竖线加入超级多段线.
1,组内只有| / 组内加入失败.
2,横线已经排序,
先找到左下横线,链接成功.成功要返回到组内.
3,组内成功连接斜线.
4,组内优先,再次组内:
扫描线击中斜线和上方横线,连接成功.
--上方横线又击中下方横线(也可能没有击中)
5,组内优先,再次组内:失败.
6,横线,左上角,成功,
7,闭环
案例4
_____
|__/|__|
前面和上面一样,
4,垂直连接成功?!
因为扫描线把斜线和上方横线归纳到同一个组,
所以组内优先原则,不会先连接垂直线.
案例5: 孤岛碰撞
直断线孤岛碰撞
a,如果纯水平和纯垂直,会分为两组.那么也存在连接问题.
b,如果其中一个斜边,导致全部是同一个组.
此图用邻接表求左下角和共点多数最近路径也是不对的,
所以排除邻接表方案?
其实这个是孤岛碰撞问题,要构造一个层次树.
可以用凸包剥开之后才构建的.
那么剥开的过程不也要邻接表吗?
那么它就是一个组内遍历的问题了,所以组内分析时候,也需要判断腰间连接.
1,每段加入的时候判断存在撞腰.
2,如果使用PointMap记录点数量,大于等于4,就是存在内环.
(剪枝之后,不可能是奇数),但是PointMap无法分析出内环和外环,
所以成为闭环之前我们进行解环,只需要根据点路径沿途获取就好了.
错误想法:
因为孤岛碰撞问题,
所以想用共点计数和左下角判断外边界,但是不行的.
无法分析出内外环的,并且完全重叠的曲线需要消重,不然会判断是出错的.
(不存部分重叠,因为会在数据清洗时候打断成多条碎线)
内环撞外环min点和max点:
外边界任何点都可能是内环的点
正确想法:
通过凸包点解/解环,才能找到最外层边界.
咖啡豆孤岛碰撞
在Acad上面的表现,画出这个图形,全部炸开.
填充命令H,然后右下角有个箭头展开,三种模式: 普通/外部/忽略.
然后: 添加: 选择对象(B)
它们对于咖啡豆孤岛碰撞
的表现不同,
会发现我们常用的普通模式
(自动分析层次关系),
居然是忽略了咖啡豆孤岛边界
,而忽略模式
才是对的...倒反天罡...
1,孤立的花瓣奇数型((),不存在的,对边成组,已经剔除了.
2,只会有偶数型(()),并且分为两组了.
目的是要做到: 移除内环之后,外环仍然成立,不存在孤立的边界造成死循环.
但是咖啡豆孤岛碰撞
会引起死循环.
当内环出现咖啡豆孤岛碰撞
,会和外环一起判断为一组,
若获取一个移除一个,这最后剩下一条曲线,
若我们用清空组来结束循环就会引起死循环.
所以递归条件不要判断组内剩余数量,
而是经过一轮(组内查找/横区查找/竖区查找)之后,没有发生提炼边界就结束.
第一次遇到咖啡豆孤岛碰撞
,发现操作这样做的话,就需要跟着桌子做.
然后我就停工了...
现在看上去,咖啡豆孤岛
貌似被认定为坏边了.
案例6 捆扎带型
当扫描线经过这个包围盒时候,存在交点为1的时候,所以需要跳过.
案例7 并行
这是单个分块,因为投影都是重叠的,无论水平还是垂直.
通过包围盒Y排序,按照CPU核心数切割数据(横线表示)
然后通过 只向后求交
,是可以进行并行的,
但是此处切蛋糕切到草莓,也就是没有区间性,
那么就是一个左闭右开的形式向后遍历了,并且通过包围盒Top可以中断,
理论上会加速的.
但是需要把Paramss改为线程安全容器.
案例8 多加入后剥离方案
案例8D剔除奇数花瓣
三个组图形
案例8A 是咖啡豆孤岛,三条线无论哪一条加入都会触发闭环.
案例8B 是正常的闭环
案例8C 是水平区和垂直区,同时存在两点满足就立即闭环情况.
横区竖区查找顺序问题:
匚 | 乛
如果按照横区优先,那么就是这样连接了: 匚乛
1,优先同时满足头尾两个端点,满足就连接.
1.1,横竖都同时满足这个条件?是可能的,见案例8C.
1.2,按照这个代码,岂不是横区和竖区可能存在多个满足?
答案是不会,因为横区和竖区已经消重了呀.
2,没有同时满足
2.1,获取一个端点的,其中一个成功就直接加入.
2.2,横竖都同时成功呢?
例如中间的竖线是直断的,那么只会遍历两次竖区,所以不可能的.
因此其实多加入边界
也会造成闭环的,腰间闭环和首尾闭环.
只是要退还回容器=>染色法,或者用set记录下来,防止重复加入.
这个方案很好√
a:难道通过邻接表查询最短路径?
邻接表耗时?并且方案缺失,改用凸包,凸包也要表...
b,难道设计两个temp path进行游走?快慢指针?
c:此时组内循环查找时候
横区竖区已经加入斜组的应该剔除,不过怎么剔除?
应该是无法剔除的,只能设置颜色,然后跳过,
一个组内颜色+100之后,就不再是0/1/2了.
public partial class ScanlineHelper {
// 1,先在同组内连接,当无法完成时候,去其他组查找.
// 那么其他组中,
// 只可能是_horizontal和_vertical内的,加速检索.
// 不可能是其他斜边组,因为会破坏对边成组规定.
// 2,已经加入的需要排除!
// 所有组是根据扫描线获取的,是含有横区和竖区的,
// 组内First可能是横线,然后再找横区,就可能是同一个,用颜色排除.
// 组内First不可能是纯色竖线,因为0x8A排除了
void FindInnerAndOuterRings(MyGroup group, SuperTree<SuperPoly> boTree) {
// 初始化加入树内,每次都从树取出
var hatchPoly = boTree.LastOrDefault();
if (hatchPoly is null) {
hatchPoly = new SuperPoly();
boTree.Add(hatchPoly);
}
if (hatchPoly.Sub.Count == 0) {
var firstInfo = group.FristOrDefault(info => info.RegionColor is 0 or 1 or 2);
// 找不到表示整个组是横组或者竖组,已经加入其他斜组
if (firstInfo is null) {
boTree.Remove(hatchPoly);
group.Clear();
return;
}
if (!ChangeInfoColor(firstInfo)) throw new("忘记过滤颜色了吗?");
group.Remove(firstInfo);
hatchPoly.TryAdd(firstInfo);
}
// 如果巡查一轮,没有改变,结束:
// 否则表示提炼成功,可以下一轮循环
// 递归不能依照组内剩余与否,否则咖啡豆会导致剩余一根,造成死循环.
int extraction = GetHashCode(boTree.Count, hatchPoly.Sub.Count);
// 组内查找
GroupFindAndAdd(group, hatchPoly);
if (group.Count == 0) return;
var hs = BinarySearch(_horizontal, hatchPoly.Y, hatchPoly.Top, hatchPoly);
var vs = BinarySearch(_vertical, hatchPoly.X, hatchPoly.Right, hatchPoly);
#if false
// 这不是必须的,因为加多了也成环,成环就会触发解环.
// 见案例8
// 1,填充边界优先首尾共满:
// 同时满足首尾点的一条曲线,因为加入它就立即闭环.
// 横区/竖区其中一条曲线即可.
// a,存在同时满足,案例8C. b,不存在两条边满足,除非没有消重.消重可以用邻接表...
var xinfo = hs.FirstOrDefault(info => info.RegionColor == 1
&& hatchPoly.HasPointFirstAndLast(info);
xinfo ??= vs.FirstOrDefault(info => info.RegionColor == 2
&& hatchPoly.HasPointFirstAndLast(info);
if (xinfo is not null) {
// 加入它立即闭环.
AddAndUnroll(boTree, hatchPoly, xinfo);
return;
}
#endif
// 2,横区查找,其中一个拐点满足
var hinfo = hs.FirstOrDefault(info => info.RegionColor == 1
&& hatchPoly.HasPointFirstOrLast(info);
if (hinfo is not null) {
AddAndUnroll(boTree, hatchPoly, hinfo);
// 组内查找,为什么立即回组内,见案例1,2,3,4
GroupFindAndAdd(group, hatchPoly);
}
// 3,竖区查找,其中一个拐点满足
var vinfo = vs.FirstOrDefault(info => info.RegionColor == 2
&& hatchPoly.HasPointFirstOrLast(info);
if (vinfo is not null) {
AddAndUnroll(boTree, hatchPoly, vinfo);
// 组内查找
GroupFindAndAdd(group, hatchPoly);
}
// 已经改变就递归,否则就是咖啡豆孤岛形状多出部分.
if (extraction != GetHashCode(boTree.Count, hatchPoly.Sub.Count)) {
FindInnerAndOuterRings(group, boTree);
}
else {
if (group.Count == 0) return;
剩下...自己完成吧
// 如果有剩下,就是咖啡豆孤岛多余部分,
// 用多叉树找到要加入的内边界然后加入.
// 不是外边界,因为外边界要保证自闭单环.
}
}
int GetHashCode(int a, int b) {
int high16Bits = (b >> 16) & 0xFFFF;
int low16Bits = b & 0xFFFF;
return (a<<16) | high16Bits | low16Bits;
}
// 组内连接,链式查找
// 1,这里岂不是也要优先: 首尾共满...多加入也闭环,所以不用.
// 拐点相同,就加入.如果加入成功,就继续加入.
// 死循环是为了同组优先原则,因为直断线在同组的.
// 否则只可能是 横区竖区 上面,例如炸开的口字,其他组会破坏对比成组原则.
void GroupFindAndAdd(MyGroup group, SuperPoly hatchPoly) {
while(true) {
var xinfo = group.FirstOrDefault(info => info.RegionColor is 0 or 1 or 2
&& hatchPoly.HasPointFirstOrLast(info);
if (xinfo is null) break;
group.Remove(xinfo); // 并行是基于不同组,所以没有线程安全问题.
AddAndUnroll(boTree, hatchPoly, xinfo); // 除了第一节,每次加入一节都要检测是否闭环
}
}
// 根据包围盒过滤先,避免遍历,用二分法
// 优化:这里需要判断n次,所以构造邻接表直接获取点边界图元就不需要遍历了啊.
IEnumerable<CurveInfo> BinarySearch(SortedList<double, MyGroup> vh,
double left, double right, SuperPoly hatchPoly) {
if (vh is null) throw new ArgumentException("vh is null");
if (vh.Count == 0) yield break;
int startIndex = GetLeft(vh.Keys, left);
int endIndex = GetRight(vh.Keys, right);
if (startIndex > endIndex) yield break;
for (int i = startIndex; i <= endIndex; i++) {
var myGroup = vh.Values[i];
foreach(var info in myGroup)
if (hatchPoly.Contains(info)) // TODO: 矩形扩展1包含
yield return info;
}
}
public int GetLeft<T>(IList<T> list, T key) where T : IComparable<T> {
if (list is null) throw new ArgumentException("list is null");
if (list.Count == 0) return 0;
int left = 0, right = list.Count - 1, result = -1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (list[mid].CompareTo(key) >= 0) {
result = mid;
right = mid - 1;
} else {
left = mid + 1;
}
}
// 优化后的重复元素查找,每次移动两步
while (result > 1 && list[result - 1].CompareTo(key) == 0) {
result -= 2;
if (result < 0 || list[result].CompareTo(key) != 0) {
result += 2;
break;
}
}
// 重复元素需要线性查找,第一个等于,找不到就是第一个大于
while (result > 0 && list[result - 1].CompareTo(key) == 0) {
result--;
}
// 不会返回-1的,最起码也是0,用于插入.
return result;
}
public int GetRight<T>(IList<T> list, T key) where T : IComparable<T> {
if (list is null) throw new ArgumentException("list is null");
if (list.Count == 0) return 0;
int left = 0, right = list.Count - 1, result = -1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (list[mid].CompareTo(key) <= 0) {
left = mid + 1;
} else {
result = mid;
right = mid - 1;
}
}
// 优化后的重复元素查找,每次移动两步
while (result < list.Count - 1 && list[result].CompareTo(key) == 0) {
result += 2;
if (result >= list.Count || list[result].CompareTo(key) != 0) {
result -= 2;
break;
}
}
// 重复元素需要线性查找,第一个大于
while (result < list.Count && list[result].CompareTo(key) == 0) {
result++;
}
// 不会返回-1的,最起码也是0,用于插入.
return result;
}
// 改颜色,用来排除已经加入过的图元
// 为什么我不放入到TryAdd内?因为想和排除写在同一个类.
bool ChangeInfoColor(CurveInfo info) {
int newValue = info.RegionColor + 100;
if (Interlocked.CompareExchange(ref info.RegionColor, newValue, 0) == 0)
return true;
if (Interlocked.CompareExchange(ref info.RegionColor, newValue, 1) == 1)
return true;
if (Interlocked.CompareExchange(ref info.RegionColor, newValue, 2) == 2)
return true;
return false;
}
// 经过前面探测,这里是肯定能加入的,只是加入头还是尾,还是撞腰而已.
// 每次加入一节,都要判断是否撞腰.表示出现内环或者外环.
void AddAndUnroll(SuperTree<SuperPoly> boTree, SuperPoly hatchPoly, CurveInfo info) {
if (!ChangeInfoColor(info)) throw new("忘记过滤颜色了吗?");
// 不撞腰就直接加入
if (!hatchPoly.HasTwoPoint(info)) {
hatchPoly.TryAdd(info);
return;
}
// 外环撞头尾: hatchPoly.IsClosed
// 外环撞腰: if (hatchPoly.HasPointFirstAndLast(info))
var bo = GetRegionLink(hatchPoly, info);
bo.TryAdd(info); // 加入就闭环
boTree.Add(bo); // 闭环加入树
hatchPoly.RemoveAll(bo); // 原本超级多段线移除闭合,之后继续找
}
// 解环:成为闭环前提取环
// p1-p2 p2-p3 p3-p4 p4-p5...我们搜索p2开始,p4结束.
// 但是不要头尾的p1-p2 p4-p5,
// 头尾只会存在一个,因为加入头尾之一才能撞腰,所以肯定有个头或尾
// 搜索的起始点和结束点,可能交换顺序/可能只击中一节曲线
// 分别对起始点/结束点计数两次,获取中间.
// 每节的点也可能交换,但是我记录是节索引,所以没问题.
SuperPoly GetRegionLink(SuperPoly old, CurveInfo cc) {
if (old is null || cc is null) throw new("参数不能为空");
int a = -1, b = -1, c = -1, d = -1;
for (int i = 0; i < old.Sub.Count; i++) {
var info = old.Sub[i];
if (info.HasPoint(cc.StartPoint)) {
if (a == -1) a = i;
else if (b == -1) b = i;
}
if (info.HasPoint(cc.EndPoint)) {
if (c == -1) c = i;
else if (d == -1) { d = i; break; }
}
}
// 存在点位交换,所以索引大小决定位置
b = b != -1 ? b : a;
d = d != -1 ? d : c;
if (b <= c) {
b = Math.Max(a, b);
c = Math.Min(c, d);
} else {
b = Math.Min(a, b);
c = Math.Max(c, d);
(b, c) =(c, b);
}
if (b == -1 || c == -1) throw new("GetRegionLink b == -1 || c == -1");
var newly = new SuperPoly();
if (b == c) newly.Add(old.Sub[b]);
else {
for (int i = b; i <= c; i++) { // 包含c
newly.Add(old.Sub[i]);
}
}
return newly;
}
}