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.
0. Vous avez un coin:
1. Vous connaissez les coordonnées des angles, que ce soit P1, P2 et P:
2. Vous pouvez maintenant obtenir des vecteurs à partir de points et angles entre vecteurs:
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 = PC1 = PC2 = rayon/| tan (angle/2) |
4. Ici, vous devez vérifier la longueur du segment et la longueur minimale de PP1 et PP2:
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:
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:
(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:
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:
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:
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);
}
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:
Pour chaque segment, construisez un vecteur normal.
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).
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.)
À 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).
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.)
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.
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.
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:
À 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.
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;
}
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;
}
Voici un moyen en utilisant une géométrie: -
- les deux lignes sont tangentes au cercle inscrit
- La normale à la tangente se rencontrent au centre du cercle.
- Laisser l'angle entre les lignes être X
- L'angle sous-tendu au centre du cercle sera K = 360-90 * 2-X = 180-X
- Permet de décider le point des deux tangentes comme (x1, y) et (x2, y)
- L’accord joignant les points a une longueur l = (x2-x1)
- A l'intérieur du cercle, la corde et deux normales de longueur r (rayon) forment un triangle isocèle
- Le pendiculaire divise le traingle en triangles rectangles égaux divisés par deux.
- L'un des angles est K/2 et le côté est l/2
- en utilisant les propriétés du triangle rectangle sin (K/2) = (l/2)/r
- r = (l/2)/sin (K/2)
- mais K = 180-X si r = (l/2)/sin (90-x/2) = (l/2)/cos (x/2)
- donc r = (x2-x1)/(2 * cos (X/2))
- 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