【问题标题】:How to handle polyline offset with variable width ("buffer")?如何处理可变宽度(“缓冲区”)的折线偏移?
【发布时间】:2022-01-06 06:19:38
【问题描述】:

我的问题陈述:

我有一条折线,我想在一个方向上沿点进行可变宽度偏移。我该怎么做?对于折线,我只需要支持直线,不需要支持曲线或圆弧。

折线可以是闭合也可以是开放的,并且偏移量只有一个方向——为了论证,我们只说它在左侧方向。

above image 几乎囊括了我想做的事情;唯一的问题是,它是整个折线的统一偏移量,因此我想要 variable 偏移量。

这个问题比最初看起来要复杂得多。有一些库不完全。让我们一一介绍。

剪辑器

Clipper 可以handle polygon buffer,这意味着在两个方向上创建偏移线,最后围绕该线形成一个多边形。所以它不适合我的需求。此外,它不处理变量缓冲。

论坛上有some discussions涉及快船开发者,但遗憾的是没有任何消息。

NetTopologySuite

NetTopologySuite 有一个VariableBuffer class,可以处理可变偏移量。但不幸的是,NetTopologySuite 只能处理多边形缓冲(将线转换为包围线的多边形),而不是 折线偏移(折线在单个方向上偏移)。

此外,NetTopologySuite 似乎使用上述方法会在两个方向上“炸毁”多边形,并且需要设置BufferParameters.IsSingleSided=true 以获得单面多边形偏移,但目前还不清楚如何使用这与VariableBuffer 结合使用。

骑士轮廓

Cavalier countours,与大多数图书馆不同,只能在一个方向上进行折线偏移(这是我想要的),这样就不会形成多边形。这是我想要的,可惜不能做变宽偏移。

如何调整当前库以满足我的需求?

似乎没有简单的方法可以做到这一点。任何想法如何做到这一点?

欢迎任何基于 C#、C++ 或 C 库构建的解决方案。

【问题讨论】:

  • “任何建立在 C#、C++ 或 C 库之上的解决方案”都非常广泛。这也是对图书馆推荐的隐含请求。如您所知,Stack Overflow 通常偏向于非常狭窄且集中的问题。如果您需要通用语言无关算法的帮助,请向我们展示您所拥有的,并使用 language-agnostic 标签而不提及特定语言或库。如果您需要有关特定库和语言的帮助,请将您的问题集中在这些细节上。提及其他尝试和库是可以的,但请尝试将问题集中在一个上。
  • @Someprogrammerdude,不,两者都不是。这不是图书馆推荐,因为我不是专门要求图书馆推荐,而是我只是要求解决我的问题,哪些图书馆是受欢迎的,这与this之类的问题没有什么不同.如今,使用库来解决编程问题是很正常的。只要您不问诸如“哪个库是最好的”之类的宽泛问题(应该关闭,因为它是主观的,而不是因为它与库本身相关),那应该没问题。
  • “我可以接受 C# 或 C++ 解决方案”——这只是意味着我可以接受这两种解决方案。所以你是说接受两种语言对你来说太宽泛了?很常见的是,一种语言的程序用于 UI,另一种语言的程序用于其他计算,并使用 pinvoke 在两者之间进行通信,因此,两者都是并且应该是可以接受的。怎么可能太宽泛了?
  • 我认为没有理由结束这个问题
  • 只是一个可能引发想法的想法:如果我们考虑图像蓝线之间的“白线”,我们可以不同地定义问题:绘制可变粗细(白)线与固定他们之间的差距。

标签: c# c++ geometry nettopologysuite clipper


【解决方案1】:

确实,使用可变偏移量时问题会更加复杂。 在具有单个偏移量的情况下,点 p 到折线的距离定义为折线的最近点到 p 的距离(例如参见 https://github.com/jbuckmccready/CavalierContours/blob/master/include/cavc/polylineoffset.hpp 中的 pointValidForOffset

对可变偏移量的推广并不明显。

如果是简单的折线(没有曲线),一个简单的解决方案是: 设 F 为初始折线。

  • 绘制G,远离变量偏移的折线(详见后文)
  • F的第一个点与G的第一个点连接(创建段A
  • F的最后一点与G的最后一点连接(创建段B
  • 我们得到一个封闭的多边形P
  • 要管理交叉点,删除循环:计算 P 与自身的并集,然后保留不是 F 的轮廓,也不是线段 A B

要绘制 G,远离变量 offset 的折线: 如果您将代码放入http://paperjs.org/“Sketch”,则此代码有效 结果如下:

/* Computes a polyline with a fixed offset (red) 
 * and another with a variable size offset (green)
 */
    const points = [
        new Point(30, 40),
        new Point(100, 80),
        new Point(200, 60),
        new Point(250, 50),
        new Point(300, 70),
        new Point(350, 90),
        new Point(400, 60),
        new Point(450, 50),
        new Point(500, 70),
        new Point(550, 90),
        ];
    
    let poly1 = new Path();
    poly1.strokeColor = 'black';
    points.forEach(p =>  poly1.add(p));
    
    const fixOffs = 5;
    const offsets = [
        10, 20, 30, 20, 10, 10, 20, 20, 30, 30];
    
    let fix = [];
    let variable = [];
    
    const size = points.length;
    for(let i=0; i<size; ++i){
        let tangent;
        if(i===0){ // if first point
            tangent = points[1] - points[0];
        }else if(i+1===size){ // if last point
            tangent = points[i] - points[i-1];
        }else{ // mean of segment before and segment after point i
            tangent = (points[i] - points[i-1]) * 0.5 +
                      (points[i+1] - points[i]) * 0.5;
        }
        let normal = new Point(-tangent.y, tangent.x); // perpendicular vector
        normal = normal / normal.length; // normalize
        fix.push(points[i] + normal * fixOffs);
        variable.push(points[i] + normal * offsets[i]);
    }
    
    let polyFix = new Path();
    polyFix.strokeColor = 'red';
    fix.forEach(p =>  polyFix.add(p));
    
    let polyVar = new Path();
    polyVar.strokeColor = 'green';
    variable.forEach(p =>  polyVar.add(p));

【讨论】:

  • draw G, the polyline far from the variable offset -- 你的第一步假设我们已经知道变量偏移的影响。但这不是问题的重点吗?
  • 我已经编辑了我的答案以详细说明这一点。您必须计算折线任意点的法线。如果曲线足够平滑,我给出的方法可以工作,否则你可能需要更复杂的方法。
  • 在我看来,您的算法甚至无法处理像凹折线这样稍微复杂的情况。
  • 对我来说,凹凸之间的唯一区别是曲线的自相交,这就是我在解决方案的以下几点中开发的。否则,我看不出任何其他区别,我错了吗?
【解决方案2】:

折线的偏移可以按如下方式进行:

  1. 首先构造一个包含初始折线并在最大偏移值上向所有方向延伸的矩形。
  2. 在小像素上细分矩形(像素大小取决于您需要的精度)。
  3. 考虑到需要更大偏移的线段的额外权重,计算从每个像素中心到折线的距离。距离必须带符号:多段线一侧为正,另一侧为负。每个像素都有距离的二维栅格有时被命名为距离图
  4. 找到距离场的等值线,这将为您提供偏移折线。

例如,这个 C++ 库提供了这里的大部分步骤:https://github.com/MeshRUs/MeshLib 当前唯一的限制是它仅计算闭合折线的带符号距离的距离图,但它可以通过多种方式解决(例如,将初始折线延伸到包含矩形之外)。

【讨论】:

  • 我不认为 OP 寻求屏幕空间解决方案,而是寻求分析解决方案。
【解决方案3】:

标准偏移算法的工作原理如下:

  1. 将线段视为实线。以给定的数量在所选方向上将它们全部偏移。
  2. 找到每对相邻线的交点。这将是顶点的新位置。
  3. 检查消失的线段并移除相应的顶点。

请注意,Jerome's code 偏移点,而不是线段。所以你正确地观察到它不能保持非常锐角的固定距离。它也完全忽略了第三步。

现在困难的部分是如何在其中包含可变偏移量?如果您为每个细分市场设置了偏移权重,那么整合将是直接的。但是在您的问题中,您声明每个顶点都有偏移权重。简单地添加将围绕获得的顶点移动的第 4 步将使第 3 步的结果无效。所以我宁愿寻找一种将变量偏移量集成到第一步的方法。事实上,正确的表示不仅需要偏移线,还需要旋转它们。它们当然不会再平行了,但在我看来,这将最好地代表可变顶点偏移性质。同样,如果您想减少此类困难,请考虑使用每段权重,这会使整个问题变得更容易,因为加权线偏移可以轻松集成到步骤 1 中。

更新

我修复并扩展了 Jerome 回答中的示例。 @Jerome,感谢您向我介绍 paper.js 以及最初的灵感。在我的版本中,我使用每个段的可变宽度,作为固定偏移的比率。 Try it out here.

//the paper.js is really stupid: the search is hidden by the execution controls
//and once I rename this class to "Line" which would be more appropriate
//(but my original intent was indeed a Halfline), it throws errors, //so I keep the slightly misleading name "Halfline"
class Halfline {
    constructor(p, r)
    {
        const tangent = r - p;
        this.p = p;
        this.r = r;
        //implicit line equation
        this.a = -tangent.y;
        this.b = tangent.x;
        this.c = p.x * this.a + p.y * this.b;
        //I didn't normalize a and b, so here a unit normal
        this.n = new Point(this.a, this.b);
        this.n /= this.n.length;
    }
    //offset the line by t in the direction of its normal
    offset(t) {
        return new Halfline(this.p + this.n * t, this.r + this.n * t)
    }
    //line intersection (infinite lines)
    intersect(other) {
        const det = this.a * other.b - other.a * this.b;
        
        if (Math.abs(det) < 1e-5) //parallel
            return undefined;
        
        const x = (other.b * this.c - this.b * other.c) / det;
        const y = (this.a * other.c - other.a * this.c) / det;
        return new Point(x, y);
    }
}

//look at this great tutorial for details on helper functions: 
//https://algorithmtutor.com/Computational-Geometry/Check-if-two-Halfline-segment-intersect/
function fixOverlaps(polyline) {
    function on_segment(p1, p2, p) {
        return Math.min(p1.x, p2.x) <= p.x
               && p.x <= Math.max(p1.x, p2.x)
               && Math.min(p1.y, p2.y) <= p.y
               && p.y <= Math.max(p1.y, p2.y);
    }
    
    function cross_product(p1, p2) {
        return p1.x * p2.y - p2.x * p1.y;
    }
    
    function direction(p1, p2, p3) {
        return cross_product(p3 - p1, p2 - p1);
    }
    
    function segmentsIntersect(p1, p2, p3, p4) {
        d1 = direction(p3, p4, p1);
        d2 = direction(p3, p4, p2);
        d3 = direction(p1, p2, p3);
        d4 = direction(p1, p2, p4);

        return ( (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0))
                 && ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0)))
            || (d1 == 0 && on_segment(p3, p4, p1))
            || (d2 == 0 && on_segment(p3, p4, p2))
            || (d3 == 0 && on_segment(p1, p2, p3))
            || (d4 == 0 && on_segment(p1, p2, p4)) );
    }
    //search for self-intersections
        let intersectionsFound = true;
    while (intersectionsFound)
    {
        let result = [polyline[0]];
        for(let i = 1; i < polyline.length; ++i)
        {
            let anyIntersection = false;
            for(let j = i + 2; j < polyline.length; ++j)
                if (segmentsIntersect(polyline[i-1], polyline[i], polyline[j-1], polyline[j]))
                {
                    const s = new Halfline(polyline[i-1], polyline[i]);
                    const t = new Halfline(polyline[j-1], polyline[j]);
                    result.push(s.intersect(t))
                    anyIntersection = true;
                    i = j;
                    break;
                }
            result.push(polyline[i])
        }
        intersectionsFound = polyline.length > result.length;
        polyline = result;
    }
    
    return polyline;
}

const points = [
        new Point(30, 40),
        new Point(100, 200),
        new Point(200, 60),
        new Point(250, 50),
        new Point(300, 70),
        new Point(350, 250),
        new Point(400, 60),
        new Point(450, 50),
        new Point(500, 70),
        new Point(550, 90),
        ];
    
    let poly1 = new Path();
    poly1.strokeColor = 'black';
    points.forEach(p => poly1.add(p));
    
    const fixOffs = 10;
    const offsets = [1, 2, 3, 2, 1, 0.5, 2.5, 2, 3, 1].map(x => x * fixOffs);
    
    let fix = [];
    let variable = [];
    
    const size = points.length;
    for(let i = 0; i < size; ++i){
        let normal;
        if(i == 0 || i == size - 1) { // first or last
            const tangent = i == 0 ? points[i + 1] - points[i] : points[i] - points[i - 1];
            const normal = new Point(-tangent.y, tangent.x) / tangent.length;
            fix.push(points[i] + normal * fixOffs);
            variable.push(points[i] + normal * offsets[i]);
        } else {
            const prevSegment = new Halfline(points[i - 1], points[i]);
            const nextSegment = new Halfline(points[i], points[i + 1]);
            //CONSTANT
            const newConstVertex = prevSegment.offset(fixOffs).intersect(nextSegment.offset(fixOffs));
            fix.push(newConstVertex || (points[i] + prevSegment.n * fixOffs));
            //VARIABLE
            const newVarVertex = prevSegment.offset(offsets[i]).intersect(nextSegment.offset(offsets[i + 1]));
            variable.push(newVarVertex || (points[i] + prevSegment.n * offsets[i]));
        }
    }

    //Resolve vanishing segments
    const finalFix = fixOverlaps(fix);
    const finalVar = fixOverlaps(variable);
    
    let polyFix = new Path();
    polyFix.strokeColor = 'red';
    finalFix.forEach(p => polyFix.add(p));/**/

    let polyVar = new Path();
    polyVar.strokeColor = 'green';
    finalVar.forEach(p => polyVar.add(p));/**/

请注意,它仍然不能处理所有特殊情况。例如,一旦偏移变得太大,非闭合多段线的角会产生奇怪的形状。另请注意,最后两个部分是并行的,Jerome 的一个有趣的选择并且有利于调试。我为它们分配了一个不同的偏移量,这迫使这些段放弃它们的并行性。

【讨论】:

  • 问题中没有提到“重量”,只有“宽度”。
  • 它们表示相同的东西 :) 我认为权重更适合一些,因为我建立在固定偏移的算法上,然后缩放每个段或每个顶点的偏移意味着 加权它。对于最直接的情况,您采用固定偏移量 = 1。
猜你喜欢
  • 2013-08-29
  • 2018-06-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-06-23
  • 1970-01-01
相关资源
最近更新 更多