web-dev-qa-db-fra.com

Algorithme de création de coins arrondis dans un polygone

Je cherche un algorithme qui me permet de créer des angles arrondis à partir d'un polygone . En entrée, j'obtiens un tableau de points représentant le polygone (ligne rouge) et en sortie, un tableau de points représentant le polygone avec coin arrondi (ligne noire). 

J'aimerais aussi avoir un moyen de contrôler le rayon de chaque coin. J'ai déjà essayé d'utiliser Bézier et Subdivision mais ce n'est pas ce que je recherche. Bézier et Subdivision lissent tout le polygone. Ce que je veux, c'est seulement rendre les coins arrondis.

Quelqu'un connaît un bon algorithme pour le faire? Je travaille en C # mais le code doit être indépendant de toutes les bibliothèques .NET.

Example

39
ZouBi

Un peu de géométrie avec Paint:


0. Vous avez un coin:
Corner

1. Vous connaissez les coordonnées des angles, que ce soit P1, P2 et P:
Points of corner

2. Vous pouvez maintenant obtenir des vecteurs à partir de points et angles entre vecteurs:
Vectors and angle

angle = atan (PY  - P1Y, PX  - P1X) - atan (PY  - P2Y, PX  - P2X)


3. Obtenir la longueur du segment entre le point angulaire et les points d'intersection avec le cercle.
Segment

segment = PC1  = PC2  = rayon/| tan (angle/2) |


4. Ici, vous devez vérifier la longueur du segment et la longueur minimale de PP1 et PP2:
Minimal length
Longueur de PP1:

PP1  = sqrt ((PX  - P1X)2  + (PY  - P1Y)2)

Longueur de PP2:

PP2  = sqrt ((PX  - P2X)2  + (PY  - P2Y)2)

Si segment> PP1 ou segment> PP2 alors vous devez diminuer le rayon:

min = Min (PP1, PP2) (pour polygone vaut mieux diviser cette valeur par 2) 
 segment> min? 
 segment = min 
 rayon = segment * | tan (angle/2) |


5. Obtenez la longueur du bon de commande:

PO = sqrt (rayon2  + segment2)


6. Obtenez le C1X et C1Y par la proportion entre les coordonnées du vecteur, la longueur du vecteur et la longueur du segment:
Coordinates of PC1

Proportion:

(PX  - C1X)/(PX  - P1X) = PC1  / PP1

Alors:

C1X  = PX  - (PX  - P1X) * PC1  / PP1

La même chose pour C1Y:

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


7. Obtenez le C2X et C2Y de la même manière:

C2X  = PX  - (PX  - P2X) * PC2  / PP2
 C2Y  = PY  - (PY  - P2Y) * PC2  / PP2


8. Maintenant, vous pouvez utiliser l'addition de vecteurs PC1 et PC2 trouver le centre du cercle de la même manière par proportion:
Addition of vectors

(PX  - OX)/(PX  - CX) = PO/PC 
 (PY  - OY)/(PY  - CY) = PO/PC

Ici:

CX  = C1X  + C2X  - PX
 CY  = C1Y  + C2Y  - PY
 PC = sqrt ((PX  - CX)2  + (PY  - CY)2)

Laisser:

dx = PX  - CX  = PX  * 2 - C1X  - C2X
 dy = PY  - CY  = PY  * 2 - C1Y  - C2Y

Alors:

PC = sqrt (dx2  + dy2) 

 OX  = PX  - dx * PO/PC 
 OY  = PY  - dy * PO/PC


9. Ici, vous pouvez dessiner un arc. Pour cela, vous devez obtenir l'angle de départ et l'angle final de l'arc:
Arc
Je l'ai trouvé ici :

startAngle = atan ((C1Y  - OY)/(C1X  - OX)) 
 endAngle = atan ((C2Y  - OY)/(C2X  - OX))


dix. Enfin, vous devez obtenir un angle de balayage et effectuer quelques vérifications:
Sweep angle

sweepAngle = endAngle - startAngle

Si sweepAngle <0, permutez startAngle et endAngle, et inversez sweepAngle:

sweepAngle < 0 ?    
    sweepAngle = - sweepAngle
    startAngle = endAngle

Vérifiez si l'angle de balayage> 180 degrés:

sweepAngle > 180 ?    
    sweepAngle = 180 - sweepAngle


11. Et maintenant, vous pouvez dessiner un coin arrondi:
The result

Un peu de géométrie avec 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));
}

Pour obtenir des points d’arc, vous pouvez utiliser ceci:

//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);
}
57
nempoBu4

Vous recherchez un arc tangent à deux segments de ligne connectés, d'un rayon donné, donnés par un tableau séquentiel de points. Le algorithm pour trouver cet arc est le suivant:

  1. Pour chaque segment, construisez un vecteur normal.

    1. Si vous travaillez en 2D, vous pouvez simplement soustraire les deux extrémités pour obtenir un vecteur tangent (X, Y). Dans ce cas, les vecteurs normaux seront plus ou moins (-Y, X). Normaliser le vecteur normal à la longueur un. Enfin, choisissez la direction avec un produit de points positif avec le vecteur tangent du segment suivant. (Voir mise à jour ci-dessous). 

    2. Si vous travaillez en 3D et non en 2D, pour obtenir la normale, croix les vecteurs tangents des deux segments au sommet que vous souhaitez arrondir pour obtenir un vecteur perpendiculaire au plan des lignes. Si la perpendiculaire a une longueur égale à zéro, les segments sont parallèles et aucun arrondi ne peut être requis. Sinon, normalisez-le, puis croisez la perpendiculaire avec la tangente pour obtenir la normale.)

  2. À l’aide des vecteurs normaux, décalez chaque segment de ligne vers l’intérieur du polygone de votre rayon souhaité. Pour décaler un segment, décaler ses extrémités en utilisant le vecteur normal N que vous venez de calculer, comme suit: P '= P + r * N (une combinaison linéaire).

  3. Intersectez les deux lignes de décalage pour trouver le centre. (Cela fonctionne parce qu'un vecteur de rayon d'un cercle est toujours perpendiculaire à sa tangente.)

  4. Pour trouver le point d'intersection du cercle avec chaque segment, décaler le centre du cercle en arrière par rapport à chaque segment d'origine. Ce seront les extrémités de votre arc.

  5. Assurez-vous que les extrémités de l’arc se trouvent à l’intérieur de chaque segment, sinon vous créerez un polygone à intersection automatique.

  6. Créez un arc à travers les deux extrémités avec le centre et le rayon que vous avez déterminés.

Je n'ai pas de logiciel de rédaction approprié sous la main, mais ce diagramme montre en quelque sorte l'idée:

enter image description here

À ce stade, vous devrez soit introduire des classes pour représenter une figure composée de segments de ligne et d'arc, soit polygoniser l'arc avec une précision appropriée et ajouter tous les segments au polygone.

Mise à jour: j'ai mis à jour l'image en indiquant les points P1, P2 et P3 et les vecteurs normaux Norm12 et Norm23. Les normales normalisées ne sont uniques que jusqu’au sens de basculement, et vous devez choisir les retournements comme suit:

  • Le produit scalaire } de Norm12 avec (P3 - P2) doit être positif. S'il est négatif, multipliez Norm12 par -1.0. Si elle est égale à zéro, les points sont colinéaires et aucun coin arrondi ne doit être créé. C'est parce que vous voulez compenser vers P3.

  • Le produit scalaire de Norm23 avec (P1 - P2) doit également être positif puisque vous vous décalez vers P1.

25
dbc

Adaptation Objective-C de nempoBu4 answer :

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

Voici ma réalisation de l'idée de dbc sur c #:

/// <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;
}
1
viter.alex

Voici un moyen en utilisant une géométrie: -

  1. les deux lignes sont tangentes au cercle inscrit
  2. La normale à la tangente se rencontrent au centre du cercle.
  3. Laisser l'angle entre les lignes être X
  4. L'angle sous-tendu au centre du cercle sera K = 360-90 * 2-X = 180-X
  5. Permet de décider le point des deux tangentes comme (x1, y) et (x2, y)
  6. L’accord joignant les points a une longueur l = (x2-x1)
  7. A l'intérieur du cercle, la corde et deux normales de longueur r (rayon) forment un triangle isocèle
  8. Le pendiculaire divise le traingle en triangles rectangles égaux divisés par deux.
  9. L'un des angles est K/2 et le côté est l/2 
  10. en utilisant les propriétés du triangle rectangle sin (K/2) = (l/2)/r
  11. r = (l/2)/sin (K/2)
  12. mais K = 180-X si r = (l/2)/sin (90-x/2) = (l/2)/cos (x/2)
  13. donc r = (x2-x1)/(2 * cos (X/2))
  14. Maintenant, dessinez simplement un arc de (x1, y) à (x2, y) en utilisant le rayon r

Remarque:-  

Ce qui précède n’est expliqué que pour les lignes qui se rencontrent à l’origine et l’axe des Y divise l’angle entre elles en deux. Mais il est également applicable à tous les coins, il suffit d’appliquer une rotation et une translation avant d’appliquer ce qui précède. De plus, vous devez sélectionner des valeurs x d'intersection à partir desquelles vous souhaitez dessiner l'arc. Les valeurs ne doivent pas être trop loin ou proches de l'origine

0
Vikram Bhat