Алгоритм создания закругленных углов в многоугольнике

Я ищу алгоритм, который позволяет мне создать закругленные углы у многоугольника. На входе я получаю массив точек, представляющий многоугольник (красная линия), а на выходе-массив точек, представляющий многоугольник с закругленным углом (черная линия).

Я также хотел бы иметь способ контролировать радиус каждого угла. Я уже пытался использовать Безье и подразделение, но это не то, что я ищу. Безье и подразделение сглаживают весь полигон. что я хочешь, только закругляй углы.

кто-нибудь знает хороший алгоритм для этого? Я работаю на C# , но код должен быть независимым от любых библиотек .NET.

Example

5 ответов


некоторая геометрия с краской:


0. У вас есть угол:
Corner

1. Вы знаете координаты угловых точек, пусть это будет Р1, P2 и P:
Points of corner

2. Теперь вы можете получить векторы из точек и угла между векторами:
Vectors and angle

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


3. Получить длину отрезка между угловой точкой и точками пересечения с круг.
Segment

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


4. Здесь нужно проверить длину сегмента и минимальную длину от PP1 и PP2:
Minimal length
Длина PP1:

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

длина PP2:

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

если сегмент > PP1 или сегмент > PP2 тогда вам нужно уменьшить радиус:

min = Min(PP1, PP2) (for polygon is better to divide this value by 2)
segment > min ?
    segment = min
    radius = segment * |tan(angle / 2)|


5. Получить длина PO:

PO = sqrt(radius2 + segment2)


6. Получить C1X и C1Y по пропорции между координатами вектора, длиной вектора и длиной сегмента:
Coordinates of PC1

долей:

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

так:

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

то же самое для C1Y:

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


7. Получить C2X и C2Y точно так же:

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


8. Теперь вы можете использовать добавление векторов PC1 и PC2 чтобы найти центр круга таким же образом по пропорции:
Addition of vectors

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

здесь:

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

допустим:

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

так:

PC = sqrt(dx2 + dy2)

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


9. Здесь можно нарисовать дугу. Для этого ты нужно получить начальный угол и конечный угол дуги:
Arc
Нашел здесь:

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


10. Наконец, вам нужно получить угол развертки и сделать некоторые проверки для него:
Sweep angle

sweepAngle = endAngle - startAngle

если 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);
}

вы ищете дугу, касательную к двум Соединенным отрезкам линии заданного радиуса, заданную некоторым последовательным массивом точек. The алгоритм найти эту дугу можно следующим образом:

  1. для каждого сегмента постройте нормальный вектор.

    1. Если вы работаете в 2d, вы можете просто вычесть две конечные точки, чтобы получить касательный вектор (X, Y). В этом случае нормальные векторы будут плюс или минус (- Y, X). нормализуют нормальный вектор к длине один. Наконец, выберите направление с положительным точечным произведением с касательным вектором следующего сегмента. (см. ниже).

    2. Если вы работаете в 3d не 2d, чтобы получить нормальный,крест касательные векторы двух сегментов в вершине вы хотите, чтобы получить перпендикулярный вектор к плоскости линии. Если длина перпендикуляра равна нулю, то отрезки параллельно и не могут быть обязательными. В противном случае нормализуйте его, затем пересеките перпендикуляр с касательной, чтобы получить Нормаль.)

  2. используя нормальные векторы, смещайте каждый сегмент линии к внутренней части многоугольника на желаемый радиус. Чтобы смещать сегмент, смещайте его конечные точки, используя только что вычисленный нормальный вектор N, например: P ' = P + r * N (линейная комбинация).

  3. пересечение двух смещений линии, чтобы найти центр. (Это работает, потому что радиус-вектор круга всегда перпендикулярен его касательной.)

  4. чтобы найти точку, в которой окружность пересекает каждый сегмент, сдвиньте центр круга назад к каждому исходному сегменту. Это будут конечные точки своей дуги.

  5. убедитесь, что конечные точки дуги находятся внутри каждого сегмента, иначе вы создадите самопересекающийся полигон.

  6. создайте дугу через обе конечные точки с центром и радиусом, которые вы определили.

У меня нет подходящего программного обеспечения для рисования, но эта диаграмма показывает идею:

enter image description here

на этом этапе вам нужно будет либо ввести классы для представления фигуры, состоящей из отрезков линии и дуги, либо полигонировать дугу с соответствующей точностью и добавить все сегменты в полигон.

Update: я обновил изображение, обозначив точки P1, P2 и P3, а также нормальные векторы Norm12 и Norm23. Нормализованные нормали уникальны только до направления сальто, и вы должны выбрать сальто следующим образом:

  • на скалярное произведение Норм12 с (P3-P2) должен быть положительным. Если он отрицательный, несколько Norm12 на -1.0. Если он равен нулю, точки коллинеарны, и закругленный угол не требуется создавать. Это потому что вы хотите смещаться в сторону P3.

  • точечное произведение Norm23 с (P1-P2) также должно быть положительным, поскольку вы смещаетесь в сторону P1.


цель-с адаптацией nempoBu4 ответ:

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;
}

вот моя реализация идеи dbc на 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. две линии касательны к вписанной окружности
  2. нормаль к касательной встречается в центре круга.
  3. пусть угол между линиями будет X
  4. угол, стягиваемый в центре круга будет K = 360-90*2-х = 180-х
  5. давайте решим две точки касательных как (x1,y) и (x2, y)
  6. аккорд, соединяющий точки, имеет длину l = (x2-x1)
  7. внутри круга хорда и две нормали длины r (радиус) образуют равнобедренный треугольник
  8. в pendicular разделить traingle на две равные половинки прямоугольный треугольники.
  9. один из угла K / 2 и сторона l / 2
  10. используя свойства прямоугольного треугольника sin (K / 2) = (l/2)/r
  11. r = (l/2)/sin (K/2)
  12. но K = 180-X so r = (l/2)/sin (90-X/2) = (l/2)/cos (X/2)
  13. следовательно, r = (x2-x1)/(2*cos (X/2))
  14. теперь просто нарисуйте дугу от (x1,y) до (x2, y), используя радиус r

Примечание:

вышеописанное объясняется только для линий, которые встречаются в начале координат, а ось Y делит угол между ними на половину. Но он одинаково применим для всех углов, просто нужно применить поворот и перевод, прежде чем применять выше. Кроме того, вам нужно выбрать некоторые значения x пересечения, из которого вы хотите нарисовать дугу . Значение не должно быть слишком далеко или близко к происхождения