Algorytm tworzenia zaokrąglonych narożników w wielokątu

Szukam algorytmu, który pozwoli mi tworzyć zaokrąglone rogi z wielokąta. W Input otrzymuję tablicę punktów reprezentującą wielokąt (czerwona linia), A W output tablicę punktów reprezentującą wielokąt z zaokrąglonym narożnikiem (czarna linia).

Chciałbym również mieć sposób, aby kontrolować promień każdego rogu. Próbowałem już użyć Beziera i Subdivision, ale nie tego Szukam. Bezier i podział wygładzają wszystkie wielokąty. What I want, to tylko rogi zaokrąglone.

Ktoś zna jakiś dobry algorytm do tego? Pracuję w C# , ale kod musi być niezależny od wszelkich bibliotek.NET.

Przykład

Author: Michael Vlasov, 2014-07-16

5 answers

Trochę geometrii z farbą:


0. Masz kącik:
Narożnik

1. Znasz współrzędne punktów narożnych, niech będzie P1, P2 I P:
Punkty narożne

2. Teraz możesz uzyskać wektory z punktów i kątów między wektorami:
Wektory i kąt

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


3. Uzyskaj długość odcinka między punktem kątowym a punktami przecięcia z okręgiem.
Segment

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


4. Tutaj musisz sprawdzić długość segmentu i minimalna długość z PP1 i PP2:
Minimalna długość
Długość PP1:

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

Długość PP2:

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

If segment > PP1 lub segment > PP2 następnie należy zmniejszyć promień:

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


5. Get the length of PO:

PO = sqrt(radius2 + segment2)


6. Get the C1X I C1Y przez proporcję między współrzędnymi wektor, długość wektora i długość odcinka:
Współrzędne PC1

Proporcja:

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

Więc:

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

To samo dla C1Y:

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


7. Get the C2X I C2Y w ten sam sposób:

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


8. Teraz możesz użyć dodawania wektorów PC1 i PC2 aby znaleźć środek okręgu w ten sam sposób przez proporcja:
Dodawanie wektorów

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

Tutaj:

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

Niech:

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

Więc:

PC = sqrt(dx2 + dy2)

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


9. Tutaj możesz narysować łuk. W tym celu musisz uzyskać kąt początkowy i kąt końcowy łuku:
Arc
Znaleziono tutaj :

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


10. W końcu musisz uzyskać kąt zamiatania i dokonać pewnych kontroli:
Kąt zamiatania

sweepAngle = endAngle - startAngle

If sweepAngle

sweepAngle < 0 ?    
    sweepAngle = - sweepAngle
    startAngle = endAngle

Sprawdź, czy zamiatanie > 180 stopni:

sweepAngle > 180 ?    
    sweepAngle = 180 - sweepAngle


11. A teraz możesz narysować zaokrąglony róg:
Wynik

Trochę geometrii z 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));
}

Aby uzyskać punkty łuku możesz użyć tego:

//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);
}
 54
Author: nempoBu4,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:18:10

Szukasz łuku stycznego do dwóch połączonych ze sobą segmentów linii, o określonym promieniu, określonym przez jakąś sekwencyjną tablicę punktów. Algorytm do znalezienia tego łuku jest następujący:

  1. Dla każdego segmentu zbuduj wektor normalny.

    1. Jeśli pracujesz w 2d, możesz po prostu odjąć dwa punkty końcowe, aby uzyskać wektor styczny (X, Y). W takim przypadku wektory normalne będą plus lub minus (- Y, X). znormalizuj wektor normalny do długości 1. Na koniec wybierz kierunek z dodatnim iloczynem punktowym z wektorem stycznym następnego segmentu. (Patrz aktualizacja poniżej).

    2. Jeśli pracujesz w 3d, a nie 2d, aby uzyskać normalny, krzyż wektory styczne dwóch segmentów na wierzchołku, który chcesz zaokrąglić, aby uzyskać prostopadły wektor do płaszczyzny linii. Jeśli prostopadła ma długość zero, segmenty są równoległe i nie może być wymagane żadne okrągłe. W przeciwnym razie znormalizuj go, a następnie przekrocz prostopadle do stycznej, aby uzyskać normalną.)

  2. Używając wektorów normalnych, przesuń każdy odcinek linii w kierunku wnętrza wielokąta o żądany promień. Aby skompensować odcinek, przesuń jego punkty końcowe za pomocą wektora normalnego N, który właśnie obliczyłeś, w następujący sposób: P' = P + r * N (kombinacja liniowa).

  3. Przeciąć dwie przesunięte linie , aby znaleźć środek. (Działa to, ponieważ wektor promienia okręgu jest zawsze prostopadły do jego styczna.)

  4. Aby znaleźć punkt, w którym okrąg przecina każdy segment, przesuń środek okręgu do tyłu do każdego oryginalnego segmentu. To będą punkty końcowe twojego łuku.

  5. Upewnij się, że punkty końcowe łuku znajdują się wewnątrz każdego segmentu, w przeciwnym razie utworzysz wielokąt samo przecinający się.

  6. Utwórz łuk w obu punktach końcowych z ustalonym środkiem i promieniem.

Nie mam żadnego odpowiedniego oprogramowania na ręka, ale ten diagram trochę pokazuje pomysł:

Tutaj wpisz opis obrazka

W tym momencie będziesz musiał albo wprowadzić klasy reprezentujące figurę składającą się z segmentów linii i łuku, albo poligonizować łuk z odpowiednią dokładnością i dodać wszystkie segmenty do wielokąta.

Aktualizacja: zaktualizowałem obraz, oznaczając punkty P1, P2 i P3 oraz wektory normalne Norm12 i Norm23. Normalni normalni są unikalne tylko do kierunku przewracania, i należy wybrać koziołki jak następuje:

  • Iloczyn punktowy normy 12 z (P3-P2) musi być dodatni. Jeśli jest ujemna, wielokrotność Norm12 przez -1,0. Jeśli jest zero, punkty są koliniowe i nie trzeba tworzyć zaokrąglonych narożników. Dzieje się tak dlatego, że chcesz wyrównać w kierunku P3.

  • Iloczyn punktowy Norm23 z (P1 - P2) musi być również dodatni, ponieważ kompensujesz w kierunku P1.

 25
Author: dbc,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-07-17 02:07:59

Objective - C adaptacja nempoBu4 odpowiedź:

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
Author: Michael Vlasov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:18:10

Oto moja realizacja pomysłu dbc na 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
Author: viter.alex,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-09-28 20:39:37

Oto sposób użycia geometrii:-

  1. dwie linie są styczne do okręgu wpisanego
  2. normalna do stycznej spotyka się w środku okręgu.
  3. niech kąt między liniami będzie x
  4. kąt nachylenia w środku okręgu będzie wynosił K = 360-90 * 2-X = 180-x
  5. pozwala określić dwa punkty styczne jako (x1, y) i (x2, y)
  6. akord łączący punkty ma długość l = (x2-x1)
  7. wewnątrz koła, akord i dwa normalne o długości r (promień) tworzą trójkąt równoramienny
  8. wahadło dzieli traingle na równe połówki trójkątów kątowych.
  9. jeden z kątów to K / 2, A bok to l / 2
  10. używanie własności trójkąta kąta prostego sin (K/2) = (l/2)/r
  11. r = (l/2) / sin (K/2)
  12. ale K = 180-x więc r = (l/2)/sin(90-X/2) = (L / 2) / cos (X / 2)
  13. stąd r = (x2-x1)/(2*cos(X/2))
  14. Teraz po prostu narysuj łuk od (x1, y) do (x2, y) używając promienia r

Uwaga:-

Powyższe jest wyjaśnione tylko dla linii, które spotykają się na początku i oś Y dzieli kąt między nimi na pół. Ale to jest równie ważne dla wszystkich rogu wystarczy zastosować obrót i tłumaczenie przed zastosowaniem powyższego. Ponadto musisz wybrać kilka wartości x przecięcia, z których chcesz narysować łuk . Wartości nie powinny być zbyt daleko lub blisko pochodzenia

 0
Author: Vikram Bhat,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-07-16 06:00:14