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;
    }

}
posted @ 2025-01-05 01:55  惊惊  阅读(586)  评论(0)    收藏  举报