web-dev-qa-db-ja.com

多角形に丸い角を作成するアルゴリズム

多角形から丸い角を作成できるアルゴリズムを探しています。入力では、ポリゴンを表すポイントの配列(赤い線)を取得し、出力では、角が丸いポリゴンを表すポイントの配列(黒い線)を取得します。

また、各コーナーの半径を制御する方法が必要です。私はすでにベジェとサブディビジョンを使用しようとしましたが、私が探しているものではありません。ベジェとサブディビジョンは、すべてのポリゴンを滑らかにします。欲しいのは、角を丸くするだけです。

誰かがそれを行うための良いアルゴリズムを知っていますか?私はC#で作業していますが、コードは.NETライブラリから独立している必要があります。

Example

41
ZouBi

ペイントを使用したジオメトリ:


0。コーナーがあります:
Corner

1。あなたはコーナーポイントの座標を知っています、それをPとしましょう1、P2 およびP:
Points of corner

2。これで、点とベクトル間の角度からベクトルを取得できます。
Vectors and angle

角度= atan(PY -P1Y、Pバツ -P1バツ)-atan(PY -P2Y、Pバツ -P2バツ


3。 angularポイントと円との交点間のセグメントの長さを取得します。
Segment

セグメント= PC1 = PC2 = radius/| tan(angle/2)|


4。ここでは、セグメントの長さとPPからの最小長を確認する必要があります1 およびPP2
Minimal length
PPの長さ1

PP1 = sqrt((Pバツ -P1バツ2 +(PY -P1Y2

PPの長さ2

PP2 = sqrt((Pバツ -P2バツ2 +(PY -P2Y2

セグメント> PPの場合1 またはセグメント> PP2 次に、半径を小さくする必要があります。

最小=最小(PP1、PP2)(ポリゴンの場合、この値を2で除算することをお勧めします)
 segment> min?
 segment = min 
 radius = segment * | tan(angle/2)|


5。 POの長さを取得します。

PO = sqrt(半径2 +セグメント2


6。 Cを入手1バツ およびC1Y ベクトルの座標、ベクトルの長さ、セグメントの長さの間の比率によって:
Coordinates of PC1

割合:

(Pバツ -C1バツ)/(Pバツ -P1バツ)= PC1 / PP1

そう:

C1バツ = Pバツ -(Pバツ -P1バツ)* PC1 / PP1

Cでも同じ1Y

C1Y = PY -(PY -P1Y)* PC1 / PP1


7。 Cを入手2バツ およびC2Y 同じ方法で:

C2バツ = Pバツ -(Pバツ -P2バツ)* PC2 / PP2
 C2Y = PY -(PY -P2Y)* PC2 / PP2


8。これで、PCの追加を使用できます1 とPC2 割合で同じ方法で円の中心を見つけるには:
Addition of vectors

(Pバツ -Oバツ)/(Pバツ -Cバツ)= PO/PC 
(PY -OY)/(PY -CY)= PO/PC

ここに:

Cバツ = C1バツ + C2バツ -Pバツ
 CY = C1Y + C2Y -PY
 PC = sqrt((Pバツ -Cバツ2 +(PY -CY2

させてください:

dx = Pバツ -Cバツ = Pバツ * 2-C1バツ -C2バツ
 dy = PY -CY = PY * 2-C1Y -C2Y

そう:

PC = sqrt(dx2 + dy2)
 
 Oバツ = Pバツ -dx * PO/PC 
 OY = PY -dy * PO/PC


9。ここで円弧を描くことができます。これには、弧の開始角度と終了角度を取得する必要があります。
Arc
それを見つけました こちら

startAngle = atan((C1Y -OY)/(C1バツ -Oバツ))
 endAngle = atan((C2Y -OY)/(C2バツ -Oバツ))


10。最後に、スイープ角度を取得し、いくつかのチェックを行う必要があります。
Sweep angle

sweepAngle = endAngle - startAngle

SweepAngle <0の場合、startAngleとendAngleを交換し、sweepAngleを反転します。

sweepAngle < 0 ?    
    sweepAngle = - sweepAngle
    startAngle = endAngle

SweepAngle> 180度かどうかを確認します。

sweepAngle > 180 ?    
    sweepAngle = 180 - sweepAngle


11。そして今、あなたは丸い角を描くことができます:
The result

C#を使用した一部のジオメトリ:

private void DrawRoundedCorner(Graphics graphics, PointF angularPoint, 
                                PointF p1, PointF p2, float radius)
{
    //Vector 1
    double dx1 = angularPoint.X - p1.X;
    double dy1 = angularPoint.Y - p1.Y;

    //Vector 2
    double dx2 = angularPoint.X - p2.X;
    double dy2 = angularPoint.Y - p2.Y;

    //Angle between vector 1 and vector 2 divided by 2
    double angle = (Math.Atan2(dy1, dx1) - Math.Atan2(dy2, dx2)) / 2;

    // The length of segment between angular point and the
    // points of intersection with the circle of a given radius
    double tan = Math.Abs(Math.Tan(angle));
    double segment = radius / tan;

    //Check the segment
    double length1 = GetLength(dx1, dy1);
    double length2 = GetLength(dx2, dy2);

    double length = Math.Min(length1, length2);

    if (segment > length)
    {
        segment = length;
        radius = (float)(length * tan);
    }

    // Points of intersection are calculated by the proportion between 
    // the coordinates of the vector, length of vector and the length of the segment.
    var p1Cross = GetProportionPoint(angularPoint, segment, length1, dx1, dy1);
    var p2Cross = GetProportionPoint(angularPoint, segment, length2, dx2, dy2);

    // Calculation of the coordinates of the circle 
    // center by the addition of angular vectors.
    double dx = angularPoint.X * 2 - p1Cross.X - p2Cross.X;
    double dy = angularPoint.Y * 2 - p1Cross.Y - p2Cross.Y;

    double L = GetLength(dx, dy);
    double d = GetLength(segment, radius);

    var circlePoint = GetProportionPoint(angularPoint, d, L, dx, dy);

    //StartAngle and EndAngle of arc
    var startAngle = Math.Atan2(p1Cross.Y - circlePoint.Y, p1Cross.X - circlePoint.X);
    var endAngle = Math.Atan2(p2Cross.Y - circlePoint.Y, p2Cross.X - circlePoint.X);

    //Sweep angle
    var sweepAngle = endAngle - startAngle;

    //Some additional checks
    if (sweepAngle < 0)
    {
        startAngle = endAngle;
        sweepAngle = -sweepAngle;
    }

    if (sweepAngle > Math.PI)
        sweepAngle = Math.PI - sweepAngle;

    //Draw result using graphics
    var pen = new Pen(Color.Black);

    graphics.Clear(Color.White);
    graphics.SmoothingMode = SmoothingMode.AntiAlias;

    graphics.DrawLine(pen, p1, p1Cross);
    graphics.DrawLine(pen, p2, p2Cross);

    var left = circlePoint.X - radius;
    var top = circlePoint.Y - radius;
    var diameter = 2 * radius;
    var degreeFactor = 180 / Math.PI;

    graphics.DrawArc(pen, left, top, diameter, diameter, 
                     (float)(startAngle * degreeFactor), 
                     (float)(sweepAngle * degreeFactor));
}

private double GetLength(double dx, double dy)
{
    return Math.Sqrt(dx * dx + dy * dy);
}

private PointF GetProportionPoint(PointF point, double segment, 
                                  double length, double dx, double dy)
{
    double factor = segment / length;

    return new PointF((float)(point.X - dx * factor), 
                      (float)(point.Y - dy * factor));
}

円弧のポイントを取得するには、これを使用できます:

//One point for each degree. But in some cases it will be necessary 
// to use more points. Just change a degreeFactor.
int pointsCount = (int)Math.Abs(sweepAngle * degreeFactor);
int sign = Math.Sign(sweepAngle);

PointF[] points = new PointF[pointsCount];

for (int i = 0; i < pointsCount; ++i)
{
    var pointX = 
       (float)(circlePoint.X  
               + Math.Cos(startAngle + sign * (double)i / degreeFactor)  
               * radius);

    var pointY = 
       (float)(circlePoint.Y 
               + Math.Sin(startAngle + sign * (double)i / degreeFactor) 
               * radius);

    points[i] = new PointF(pointX, pointY);
}
63
nempoBu4

ポイントの連続した配列によって与えられる、指定された半径の2つの接続されたラインセグメントに接するアークを探しています。この弧を見つけるためのalgorithmは次のとおりです。

  1. 各セグメントに対して、法線ベクトルを作成します。

    1. 2Dで作業している場合は、2つの端点を差し引くだけで接線ベクトル(X、Y)を取得できます。その場合、法線ベクトルはプラスまたはマイナス(-Y、X)になります。 正規化 長さ1の法線ベクトル。最後に、次のセグメントの接線ベクトルと正の内積を持つ方向を選択します。 (以下の更新を参照してください)。

    2. 2Dではなく3Dで作業している場合、法線を取得するには、 cross 丸める頂点の2つのセグメントの接線ベクトルを使用して、ラインの平面に垂直なベクトルを取得します。垂線の長さがゼロの場合、セグメントは平行であり、ラウンドは必要ありません。それ以外の場合は、正規化してから、法線を取得するために接線で垂線を交差させます。)

  2. 法線ベクトルを使用して、各ラインセグメントを目的の半径だけポリゴンの内側にオフセットします。セグメントをオフセットするには、P '= P + r * N(線形結合)のように、計算したばかりの法線ベクトルNを使用してその端点をオフセットします。

  3. 2本のオフセット線を交差させる 中心を見つけます。 (これは、円の半径ベクトルが常に接線に対して垂直であるため機能します。)

  4. 円が各セグメントと交差する点を見つけるには、元の各セグメントに対して円の中心を後方にオフセットします。これらは、アークのエンドポイントになります。

  5. 円弧の終点が各セグメント内にあることを確認してください。そうでない場合は、自己交差するポリゴンを作成します。

  6. 決定した中心と半径の両方の端点を通る円弧を作成します。

適切な製図ソフトウェアは手元にありませんが、次の図はそのアイデアを示しています。

enter image description here

この時点で、線分と円弧セグメントで構成される図を表すクラスを導入するか、適切な精度で円弧を多角化してすべてのセグメントを多角形に追加する必要があります。

更新:イメージを更新し、ポイントP1、P2、およびP3、および法線ベクトルNorm12およびNorm23にラベルを付けました。正規化された法線は、反転方向までのみ一意であり、次のように反転を選択する必要があります。

  • ドット積 (P3-P2)のNorm12は正でなければなりません。負の場合、-1.0で複数のNorm12。ゼロの場合、ポイントは同一直線上にあり、角を丸くする必要はありません。これは、P3に向かってオフセットするためです。

  • Norm23と(P1-P2)の内積も正でなければなりません。これは、P1に向かってオフセットしているためです。

26
dbc

nempoBu4 answer のObjective-C適応

typedef enum {
    path_move_to,
    path_line_to
} Path_command;





static inline CGFloat sqr (CGFloat a)
{
    return a * a;
}





static inline CGFloat positive_angle (CGFloat angle)
{
    return angle < 0 ? angle + 2 * (CGFloat) M_PI : angle;
}





static void add_corner (UIBezierPath* path, CGPoint p1, CGPoint p, CGPoint p2, CGFloat radius, Path_command first_add)
{
    // 2
    CGFloat angle = positive_angle (atan2f (p.y - p1.y, p.x - p1.x) - atan2f (p.y - p2.y, p.x - p2.x));

    // 3
    CGFloat segment = radius / fabsf (tanf (angle / 2));
    CGFloat p_c1 = segment;
    CGFloat p_c2 = segment;

    // 4
    CGFloat p_p1 = sqrtf (sqr (p.x - p1.x) + sqr (p.y - p1.y));
    CGFloat p_p2 = sqrtf (sqr (p.x - p2.x) + sqr (p.y - p2.y));
    CGFloat min = MIN(p_p1, p_p2);
    if (segment > min) {
        segment = min;
        radius = segment * fabsf (tanf (angle / 2));
    }

    // 5
    CGFloat p_o = sqrtf (sqr (radius) + sqr (segment));

    // 6
    CGPoint c1;
    c1.x = (CGFloat) (p.x - (p.x - p1.x) * p_c1 / p_p1);
    c1.y = (CGFloat) (p.y - (p.y - p1.y) * p_c1 / p_p1);

    //  7
    CGPoint c2;
    c2.x = (CGFloat) (p.x - (p.x - p2.x) * p_c2 / p_p2);
    c2.y = (CGFloat) (p.y - (p.y - p2.y) * p_c2 / p_p2);

    // 8
    CGFloat dx = p.x * 2 - c1.x - c2.x;
    CGFloat dy = p.y * 2 - c1.y - c2.y;

    CGFloat p_c = sqrtf (sqr (dx) + sqr (dy));

    CGPoint o;
    o.x = p.x - dx * p_o / p_c;
    o.y = p.y - dy * p_o / p_c;

    // 9
    CGFloat start_angle = positive_angle (atan2f ((c1.y - o.y), (c1.x - o.x)));
    CGFloat end_angle = positive_angle (atan2f ((c2.y - o.y), (c2.x - o.x)));


    if (first_add == path_move_to) {
        [path moveToPoint: c1];
    }
    else {
        [path addLineToPoint: c1];
    }
    [path addArcWithCenter: o radius: radius startAngle: start_angle endAngle: end_angle clockwise: angle < M_PI];
}





UIBezierPath* path_with_rounded_corners (NSArray<NSValue*>* points, CGFloat corner_radius)
{
    UIBezierPath* path = [UIBezierPath bezierPath];
    NSUInteger count = points.count;
    for (NSUInteger i = 0; i < count; ++i) {
        CGPoint prev = points[i > 0 ? i - 1 : count - 1].CGPointValue;
        CGPoint p = points[i].CGPointValue;
        CGPoint next = points[i + 1 < count ? i + 1 : 0].CGPointValue;
        add_corner (path, prev, p, next, corner_radius, i == 0 ? path_move_to : path_line_to);
    }
    [path closePath];
    return path;
}
6
Michael Vlasov

いくつかのジオメトリを使用する方法は次のとおりです。

  1. 2本の線は内接円に接しています
  2. 接線の法線は円の中心で交わります。
  3. 線間の角度をXとする
  4. 円の中心で定められた角度はK = 360-90 * 2-X = 180-Xになります
  5. 接線の2点を(x1、y)と(x2、y)として決定しましょう
  6. 点を結ぶ弦の長さはl =(x2-x1)です
  7. 円の内側で、弦と長さr(半径)の2つの法線は二等辺三角形を形成します
  8. 垂線は、トレーンを半分に等しい直角三角形に分割します。
  9. 角度の1つはK/2で、辺は1/2
  10. 直角三角形のプロパティを使用してsin(K/2)=(l/2)/ r
  11. r =(l/2)/ sin(K/2)
  12. ただし、K = 180-Xなので、r =(l/2)/ sin(90-X/2)=(l/2)/ cos(X/2)
  13. したがって、r =(x2-x1)/(2 * cos(X/2))
  14. 半径rを使用して、(x1、y)から(x2、y)に弧を描くだけです

注:-

上記は、原点で交わるラインについてのみ説明されており、Y軸はそれらの間の角度を半分に分割します。ただし、上記を適用する前に回転と平行移動を適用するだけで、すべてのコーナーに等しく適用できます。さらに、円弧を描く場所から交差点のx値を選択する必要があります。値はOriginに近すぎたり遠すぎたりしてはいけません

0
Vikram Bhat

これは、c#でのdbcのアイデアの私の実現です。

/// <summary>
/// Round polygon corners
/// </summary>
/// <param name="points">Vertices array</param>
/// <param name="radius">Round radius</param>
/// <returns></returns>
static public GraphicsPath RoundCorners(PointF[] points, float radius) {
    GraphicsPath retval = new GraphicsPath();
    if (points.Length < 3) {
        throw new ArgumentException();
    }
    rects = new RectangleF[points.Length];
    PointF pt1, pt2;
    //Vectors for polygon sides and normal vectors
    Vector v1, v2, n1 = new Vector(), n2 = new Vector();
    //Rectangle that bounds arc
    SizeF size = new SizeF(2 * radius, 2 * radius);
    //Arc center
    PointF center = new PointF();

    for (int i = 0; i < points.Length; i++) {
        pt1 = points[i];//First vertex
        pt2 = points[i == points.Length - 1 ? 0 : i + 1];//Second vertex
        v1 = new Vector(pt2.X, pt2.Y) - new Vector(pt1.X, pt1.Y);//One vector
        pt2 = points[i == 0 ? points.Length - 1 : i - 1];//Third vertex
        v2 = new Vector(pt2.X, pt2.Y) - new Vector(pt1.X, pt1.Y);//Second vector
        //Angle between vectors
        float sweepangle = (float)Vector.AngleBetween(v1, v2);
        //Direction for normal vectors
        if (sweepangle < 0) { 
            n1 = new Vector(v1.Y, -v1.X);
            n2 = new Vector(-v2.Y, v2.X);
        }
        else {
            n1 = new Vector(-v1.Y, v1.X);
            n2 = new Vector(v2.Y, -v2.X);
        }

        n1.Normalize(); n2.Normalize();
        n1 *= radius; n2 *= radius;
        /// Points for lines which intersect in the arc center
        PointF pt = points[i];
        pt1 = new PointF((float)(pt.X + n1.X), (float)(pt.Y + n1.Y));
        pt2 = new PointF((float)(pt.X + n2.X), (float)(pt.Y + n2.Y));
        double m1 = v1.Y / v1.X, m2 = v2.Y / v2.X;
        //Arc center
        if (v1.X == 0) {// first line is parallel OY
            center.X = pt1.X;
            center.Y = (float)(m2 * (pt1.X - pt2.X) + pt2.Y);
        }
        else if (v1.Y == 0) {// first line is parallel OX
            center.X = (float)((pt1.Y - pt2.Y) / m2 + pt2.X);
            center.Y = pt1.Y;
        }
        else if (v2.X == 0) {// second line is parallel OY
            center.X = pt2.X;
            center.Y = (float)(m1 * (pt2.X - pt1.X) + pt1.Y);
        }
        else if (v2.Y == 0) {//second line is parallel OX
            center.X = (float)((pt2.Y - pt1.Y) / m1 + pt1.X);
            center.Y = pt2.Y;
        }
        else {
            center.X = (float)((pt2.Y - pt1.Y + m1 * pt1.X - m2 * pt2.X) / (m1 - m2));
            center.Y = (float)(pt1.Y + m1 * (center.X - pt1.X));
        }
        rects[i] = new RectangleF(center.X - 2, center.Y - 2, 4, 4);
        //Tangent points on polygon sides
        n1.Negate(); n2.Negate();
        pt1 = new PointF((float)(center.X + n1.X), (float)(center.Y + n1.Y));
        pt2 = new PointF((float)(center.X + n2.X), (float)(center.Y + n2.Y));
        //Rectangle that bounds tangent arc
        RectangleF rect = new RectangleF(new PointF(center.X - radius, center.Y - radius), size);
        sweepangle = (float)Vector.AngleBetween(n2, n1);
        retval.AddArc(rect, (float)Vector.AngleBetween(new Vector(1, 0), n2), sweepangle);
    }
    retval.CloseAllFigures();
    return retval;
}
0
viter.alex