2

我的问题陈述:

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

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

在此处输入图像描述几乎囊括了我想做的事情;唯一的事情是,它是整个折线的统一偏移,因此我想要可变偏移。

这个问题比最初看起来要棘手得多。有一些图书馆不太这样做。让我们一一介绍。

快船

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

快船开发者在论坛上对此进行了一些讨论,但不幸的是没有任何结果。

网络拓扑套件

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

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

骑士轮廓

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

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

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

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

4

3 回答 3

0

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

对可变偏移量的概括并不明显。

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

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

要绘制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));
于 2022-01-06T17:02:32.570 回答
0

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

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

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

于 2022-01-16T11:38:58.907 回答
0

标准偏移算法将按如下方式工作:

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

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

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

更新

我从杰罗姆的回答中修复并扩展了这个例子。@Jerome,感谢您向我介绍 paper.js 以及最初的灵感。在我的版本中,我使用每个段的可变宽度,作为固定偏移的比率。在这里尝试一下。

//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 的一个有趣的选择,并且有利于调试。我为它们分配了一个不同的偏移量,这迫使这些段放弃它们的平行性。

于 2022-01-15T23:54:34.917 回答