在基于DCEL /半边的图形中动态添加边吗?

时间:2019-07-11 00:30:54

标签: algorithm graphics graph-theory

我正在尝试实现矢量图形“草稿”系统,从本质上讲,用户可以在屏幕上绘制线条并与由相交的线条创建的区域进行交互。我正在努力确定/评估这些区域是什么。

我已经尝试了一些解决此问题的方法,主要是保留边缘列表并运行BFS以找到最短的周期,但这带来了无数的问题,其中BFS将以非法方式捷径,漏洞和退化的边缘导致更多的问题超出我的想象,所以我转向DCEL半边缘系统。

我似乎已经阅读了有关该主题的所有内容,其中包括两篇在此频繁引用的文章:http://kaba.hilvi.org/homepage/blog/halfedge/halfedge.htmhttp://www.flipcode.com/archives/The_Half-Edge_Data_Structure.shtml。但是,这些都似乎都无法解决我向图形动态添加边时遇到的这个问题。

比方说,我从这个单一优势开始。 Image

半边以一个周期彼此连接,并且全局无边界的“外表面”连接到半边之一。容易,知道了。

然后,我们添加附加到中心顶点的另一条边:Image

新的半边效果很好,我们将流入v1的下一个指针的边更新为仅有的不是其双胞胎的其他边。再说一次,对我来说很有意义。

当我们向中心顶点添加第三条边时,Image

使我无休止地发生了什么?

我知道这是应有的并链接起来,但是我对如何以编程方式实现这一目标感到困惑,因为我不确定如何确定边缘(4,1)应该指向边(1,2)或边(1,3)(类似,哪个边应该指向(1,4))。

当查看图像时,答案似乎显而易见,但是当您尝试以健壮,密封的算法合理化图像时,我的大脑就会融化,无法弄清楚。我正在阅读的教科书(《计算几何》,马克·德伯格等人,第35页)只是说

  

“ [[测试边缘的位置]应该处于边缘的循环顺序中   围绕顶点v”。

hilvi.org文章中给出的用于查找要链接的传出和传入边缘的算法甚至似乎都不起作用,因为它将采用顶点1,并遵循其传出边缘的孪生子,直到找到“自由”。 ” edge,在这种情况下为(2,1),这是错误的。 (除非我不正确地理解它,否则我可能会错误地理解整个问题。)

所以我绝对很沮丧。我现在唯一的想法是为每个半边创建某种航向属性,在这里我测量由边创建的角度,然后以这种方式选择边,也许是正确的,但这似乎与半边结构相反似乎支持,至少在我正在阅读的文章中似乎没有提及。任何帮助将不胜感激。我已经在这个问题上待了一个多星期了,似乎无法释怀。

1 个答案:

答案 0 :(得分:1)

是的,所以我花了很多时间思考这个问题,老实说,我为无法找到这个问题的直接答案感到惊讶。因此,如果将来有人遇到类似的问题,希望从头开始填充半边图,那么这是一个可行的解决方案。我没有博客,所以在这里写。

我不知道这是否是最好的答案,但是它在线性时间内有效,对我来说似乎很简单。

我将处理以下与常规DCEL略有不同的对象/类:

class Vertex {
    x;
    y;

    edges = []; //A list of all Half Edges with their origin at this vertex.
                //Technically speaking this could be calculated as needed, 
                  and you could just keep a single outgoing edge, but I'm not 
                  in crucial need of space in my application so I'm just 
                  using an array of all of them.
}


class HalfEdge {
    origin; //The Vertex which this half-edge emanates from

    twin; // The half-edge pair to this half-edge

    face; // The region/face this half-edge is incident to

    next; // The half-edge that this half-edge points to
    prev; // The half-edge that points to this half-edge

    angle; //The number of degrees this hedge is CW from the segment (0, 0) -> (inf, 0)
}


class Face {
    outer_edge; //An arbitrary half-edge on the outer boundary defining this face.
    inner_edges = []; //A collection of arbitrary half-edges, each defining
                      //A hole in the face.

    global; //A boolean describing if the face is the global face or not.
            //This could also be done by having a single "global face" Face instance. 
            //This is simply how I did it.
}

用于初始化(x,y)处的顶点:

  1. 验证具有给定(x,y)坐标的顶点不存在。如果是这样,则您无需执行任何操作(除非立即使用它,否则可以返回此现有顶点)。

  2. 如果没有,请为其分配空间并创建一个具有相应x,y值且其入射边为null的新顶点。

用于初始化从顶点A到顶点B的边:

  1. 与许多与此主题相关的文章类似,我们创建了HalfEdge的两个新实例,一个实例从顶点A到B,一个实例从B到A。它们彼此链接,因为我们将它们的孪生关系设置为prev ,所有下一个指针都指向另一半边(对冲)。

  2. 我们还设置了树篱的角度。从正x轴顺时针计算角度。我实现的功能如下。这对于使此数据结构正常工作非常重要,而且我在文献中没有读过任何东西,这一点很重要,这使我认为必须更好的方法,但我离题了。

        setAngle(){
            const dx = this.destination().x - this.origin.x;
            const dy = this.destination().y - this.origin.y;
    
            const l = Math.sqrt(dx * dx + dy * dy);
            if (dy > 0) {
                this.angle = toDeg(Math.acos(dx / l));
            } else {
                this.angle = toDeg(Math.PI * 2 - Math.acos(dx / l));
            }
    
             function toDeg(rads) {
                 return 180 * rads / Math.PI;
             }
    
         }
    
  3. 接下来,我们将顶点与新边缘配对,方法是将其添加到“顶点”的边缘列表中,然后根据树篱的角度对边缘列表进行排序,从最小(0)到最大(359)。

  4. 然后这是关键步骤,为了正确地链接所有内容,我们将最接近的树篱与要按CCW顺序链接的新树篱进行抓取。基本上,无论新对冲最终出现在边缘列表中的是index - 1(如果index = 0,我们都返回edges[edges.length - 1])。以那个边缘的双胞胎为例,这成为我们上面在hivli文章中描述的AIn。 BOut = AIn.next

  5. 我们设置AIn.next = hedgeAB并类似地设置hedgeAB.prev = AIn,然后设置hedgeBA.next = AOutAOut.prev = hedgeBA。除了在顶点B上运行CCW搜索外,还对树篱BA执行步骤3-5。

  6. 然后,如果顶点A和B都是“旧”顶点,这意味着它们的边列表现在每个都有至少2个元素,则可能会添加一个新面 ,我们需要找到它(边缘盒具有两个孤立的边缘,并将它们连接起来以创建无界的桶形或帽形)

用于初始化人脸:

  1. 我们需要找到图中的所有循环。对于我的第一个实现,我每次都重新计算所有循环,重置所有面孔。这不是必需的,但也不会太昂贵,因为我们没有运行搜索,所以相对于循环数和每个循环中的顶点数,所有内容都是线性时间

  2. 为此,我们获得了图中所有树篱的列表。这样做并不重要,我决定保留每次传递给循环查找器函数的每个树篱的数组。

  3. 然后,我们浏览该列表,但列表不为空时,我们获取第一个项目并运行其循环,从列表中删除沿途找到的所有对冲,然后将其添加到新循环中,我们将其添加到另一个列表中

  4. 使用这个新的循环列表,我们需要确定该循环是内部循环还是外部循环。有很多方法可以做到这一点,并且上面提到的《计算几何》一书对此有很大的介绍。我使用的是计算每个循环定义的面积。如果面积> = 0,则循环由“内部”树篱定义。否则,它是由“外部”树篱定义的。

  5. 最后一步是设置所有面部记录,同样,上述教科书对此有很多详细说明,但是基本思想是基本上创建这些循环的虚拟“图形”并进行连接外循环(它们是面中的孔)到其对应的内部循环(即面的外边界)。为此,请查看循环的最左侧顶点,然后将射线无限地向左延伸,然后将循环与射线命中的循环的第一个朝下的树篱“连接”起来(我将实现保留在前面)对您来说,我不知道我的方法是否是最好的,总之,我检查了每个循环中当前循环最左边的顶点,并计算出与当前循环最左边顶点的y值最右边的交点,然后检查是否朝下)。

  6. 使用此周期图,从每个“内部对冲”周期(而不是孔)开始运行BFS / DFS,并从内部对冲周期创建具有任意对冲的面作为外沿,(如果是全局面,则为null),以及从每个找到的空穴循环到面的内部组件的任意树篱。

嘿,就是这样。如果您每次都检查所有内容,则可以处理所有内容。它可以像迷人一样处理面部分裂,并且非常健壮和快速。我不知道它是否正确,但是可以。