如何计算多边形的圆角?

58
我正在寻找一种算法,能使我从多边形创建圆角。我有一个点数组表示多边形(以红色轮廓显示),输出时我希望获得一个点数组,表示带有圆角的多边形(以黑色轮廓显示)。我还想有一种控制每个角半径的方法。我尝试使用贝塞尔曲线和细分,但这不是我要找的。贝塞尔曲线和细分会平滑多边形。我想要的是只让角变圆。请问有人知道做到这点的好算法吗?我正在使用C#,但代码必须独立于任何.NET库。

给定一个半径R,找到与两个相邻线段相切的圆。圆心位于角平分线上,其中t=R/sin(a/2),其中t是圆心到角点的距离,a是角的大小。 - xiaofeng.li
7个回答

96

使用Paint进行几何学:


0. 你有一个角落:
Corner

1. 你知道角点的坐标,假设它是P1,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 = 半径 / |tan(角度 / 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)(对于多边形最好将此值除以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 = 根号下(dx的平方 + dy的平方)
OX = PX - dx * PO / PC OY = PY - dy * PO / PC


9. 在这里,您可以绘制一个弧线。为此,您需要获取弧线的起始角度和结束角度:
Arc
这里找到:

startAngle = atan((C1Y - OY) / (C1X - OX))
endAngle = atan((C2Y - OY) / (C2X - OX))
最后,您需要获得一个扫描角度并进行一些检查:
扫描角度
sweepAngle = endAngle - startAngle

如果sweepAngle < 0,则交换startAngle和endAngle,并反转sweepAngle:
sweepAngle < 0 ?    
    sweepAngle = - sweepAngle
    startAngle = endAngle

检查扫描角度是否大于180度:

sweepAngle > 180 ?    
    sweepAngle = 180 - sweepAngle


11. 现在你可以绘制圆角:
结果

使用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);
}

3
谢谢!它能完美地工作! dbc的回答解释了方法,而您的回答提供了实现。很遗憾我无法验证您两个答案。对于那些想要生成点而不是使用图形库绘制弧线的人,以下是代码:PointF[] points = new PointF[pointsCount]; for(int i=0; i - ZouBi
@ZouBi 我在sweepAngle的额外检查中纠正了错误(请查看新代码),并使用您的代码进行了一些更改后更新了我的答案。我的算法与dbc的算法不同。 - nempoBu4
这是我见过的最好的解释来回答一个疑问!太棒了 @nempoBu4! - Nane
3
如果有人遇到和我一样的问题,我需要更改 if (sweepAngle > Math.PI) sweepAngle = Math.PI - sweepAngle;if (sweepAngle > Math.PI) sweepAngle = -(2 * Math.PI - sweepAngle); 来解决曲线缺失的问题。请注意,这两行代码的含义不变,只是数学计算方式略有不同。 - Martin
1
@Mate,这个逻辑来自于向量加法公式(https://onlinemschool.com/math/library/vector/add_subtract/)`a + b = {ax + bx; ay + by}。其中 ax = C1x - Pxbx = C2x - Pxax + bx = Cx - Px=>Cx - Px = C1x - Px + C2x - Px=>Cx = C1x + C2x - Px`。 - nempoBu4
显示剩余9条评论

35
你正在寻找一个弧度,它与两个连接线段的半径相同,并由一些顺序数组中的点给出。以下是查找此弧度的算法:
  1. 对于每个线段,构造一个法向量。
    1. 如果你在二维平面上工作,只需将两个端点相减即可得到一个切向量(X,Y)。 在这种情况下,法向量将是加或减的(-Y,X)。Normalize法向量为长度为1。最后,选择与下一段切向量具有正点积的方向。(见下面的更新)。
    2. 如果你不是在二维平面上工作而是在三维平面上工作,要获得法向量,请cross两个线段在你想要圆角的顶点处的切向量,以获得线段平面的垂直向量。如果垂直向量长度为零,则线段平行,不需要圆角。否则,将其标准化,然后将垂直向量与切向量交叉以获得法向量。
  2. 使用法向量,通过所需半径向多边形内部偏移每个线段。要偏移一个线段,请使用刚刚计算的法向量N来偏移其端点,如下:P' = P + r * N(线性组合)。
  3. Intersect the two offset lines以找到圆心。(这是因为圆的半径矢量始终垂直于其切线。)
  4. 要找到圆与每个线段相交的点,请将圆心向后偏移到每个原始线段。这些将是你的弧的端点。
  5. 确保弧的端点位于每个线段内部,否则你将会创建一个自交多边形。
  6. 通过确定的圆心和半径通过两个端点创建一个圆弧。

我手头没有合适的绘图软件,但这张图大致展示了这个想法:

enter image description here

此时,您需要引入类来表示由线段和弧段组成的图形,或将弧段多边化为适当的精度并将所有段添加到多边形中。

更新:我已更新图像,标记点P1、P2和P3以及法向量Norm12和Norm23。归一化的法向量仅在翻转方向时是唯一的,并且您应按以下方式选择翻转:

  • Norm12与(P3-P2)的点积必须为正数。如果它为负,则将Norm12乘以-1.0。如果为零,则点共线,不需要创建圆角。这是因为您想向P3偏移。

  • Norm23与(P1-P2)的点积也必须为正数,因为您正在向P1偏移。


谢谢,我理解你想让我这样做的方式。 但是我现在有一个问题:如何将一条线向多边形内部偏移? - ZouBi
@ZouBi 基本上,这条线总是会与另外两条线相交。也许你可以检查一下。 - チーズパン
1
dbc,感谢您的编辑。我认为这是最好的答案,我会尝试编写代码来实现它。 - ZouBi
@JakeStelman - 我注意到你的编辑被拒绝了,但如果你愿意,你可以将你的Matlab代码作为一个单独的答案添加进去。它看起来非常有用! - dbc

11

这是适用于Objective-C的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;
}

这是一个关于C#的问题,不是Objective C的问题。 - Teepeemm
10
@Teepeemm,你对C#的看法是正确的,但nempoBu4给出的精彩答案对我的iOS开发有所帮助。像我这样的许多iOS和Mac OS开发者,通过谷歌搜索访问此页面。我们的目标是为了帮助他们。 - Michael Vlasov
http://meta.stackoverflow.com/q/290046/2336725 可能是一个有用的参考。我不知道 Objective C 和 C# 有多大的不同。你的实现除了简单地更改编程语言之外,是否还添加了其他内容?此外,您可能需要删除所有额外的空白行。 - Teepeemm
3
我的改编对原始算法进行了轻微的更改:1)角度转换为正值;2)iOS使用不同的方式定义弧(起始角度、结束角度和顺时针标志),而不是.NET(起始角度、扫描角度)。3)我的算法构建带有圆角的完整闭合图形路径,而不是在角落里绘制弧线。 - Michael Vlasov

7

我可以提供一种简单、可计算和可编程的方法,该方法只使用了最少的计算,包括“仅”3个平方根和没有任何反三角函数。

请不要因为下面故意详细的解释而感到畏惧,我写成这样是为了确保这种绝对微不足道(与此同时其他所有解决方案在此撰写时都要复杂得多)算法能够被理解。实际上,在承认替代方案所需的算法和计算复杂性(其中包括多次调用反三角函数(它们隐藏了许多计算复杂性在它们貌似无害的名称背后)以及更大量的操作)之后,我才想出这个算法。

(我已经通过JavaScript和SVG编程验证了所提出的方法的有效性。我将使用前者来帮助说明这种方法)

假设你想要“圆角”的某个拐角由已知点ABC组成,其中B是“拐角”。

Points A, B and C with lines from A to B and from B to C; a circle that includes the arc for the rounded corner, at point O as its center, line perpendicular to the vector AB going from O to meet some point F between A and B

该解决方案可以通过以下步骤描述:

  1. 计算 BF 向量的长度:

    长度等于你选择的圆的半径(FO)除以向量 BFBO 之间夹角的正切值。这是因为由点 BOF 组成的三角形是一个“直角”三角形(向量 BFFO 之间的夹角为 90 度)。

    向量 BFBO 之间的夹角是向量 BABC 之间夹角的一半。这可能听起来很明显,但可以肯定地证明它是微不足道的,但我省略了证明。

    角度之间的关系很有用,因为恰好存在一个相当简单的方程式,表达了角度的正切和两倍角度的余弦之间的关系:Math.tan(a/2) == Math.sqrt((1 - Math.cos(a)) / (1 + Math.cos(a)))

    恰好,向量 BABC 之间的夹角的余弦(Math.cos(a))是两个向量的点积除以它们长度的乘积(请参见 Wikipedia 上向量点积的定义)。

    因此,计算出角度的余弦值后,您可以计算出半角度的正切值,随后计算出 BF 的长度:

    (图例:我将向量(BABC 等)建模为具有属性 xy 的对象,用于屏幕空间中的相应坐标(X 向右增加,Y 向下增加);radius 是所需圆角的半径,BF_lengthBF 的长度(显然))

    /// 辅助函数
    const length = v => Math.sqrt(v.x * v.x + v.y * v.y);
    const dot_product = (v1, v2) => v1.x * v2.x + v1.y * v2.y;
    const cosine_between = (v1, v2) => dot_product(v1, v2) / (length(v1) * length(v2));
    
    const cos_a = cosine_between(BA, BC);
    const tan_half_a = Math.sqrt((1 - cos_a) / (1 + cos_a));
    const BF_length = radius / tan_half_a;
    
  2. 计算 BF 向量。我们现在知道它的长度(上面的 BF_lengthBF 与向量 BA 在同一条直线上,因此可以通过将 BA 的单位向量进行标量乘法来计算前者(以及由此推导出的点 F 相对于点 B 的坐标):

    /// 辅助函数
    const unit = v => {
        const l = length(v);
        return { x: v.x / l, y: v.y / l };
    };
    const scalar_multiply = (v, n) => ({ x: v.x * n, y: v.y * n });
    
    const BF = scalar_multiply(unit(BA), BF_length);
    
  3. 现在您已经从前一步骤中得到

    这就是全部内容——由于您已经在与原始点(ABC)具有相同空间坐标的点O,您可以将使用的半径圆心放在O处。

    可选项

    计算从点F和一些F'(其等效于BC向量上的点)得出相应的圆弧应该相当容易,但除非有人要求,否则我不会包括它。

    术语

    这对于大多数使用此答案的人可能是显而易见的,但为了安全起见,请记住,在此答案中,我通常将向量和坐标称为同一种度量方式--向量具有元数,即其组件数量;对于二维坐标系统,元数显然为2。因此,向量对象并没有特别编码其“起点”,只有“终点”--由于只有两个分量,暗示着向量“起点”在坐标系原点。例如,向量BA确实是点BA之间的向量,但由于程序仅存储向量的两个分量(代码段中的xy),因此就好像将向量移动,使得点B现在位于坐标系原点。点也由两个分量组成,因此“向量”和“点”是可互换的。您必须非常清楚地理解这一点,否则我提供的某些计算可能会在某些时候看起来很奇怪。如果您将向量视为每个具有两个元素的“一维”数组,则可能更容易。实际上,这就是我最初编写这些内容的方式,但出于说明代码解决方案的目的,我切换到了具有xy属性的对象。

1
这是我对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;
}

0

这里有一种使用几何学的方法:

  1. 这两条直线与内切圆相切
  2. 正切线的法线在圆心处相交。
  3. 设直线间夹角为X
  4. 圆心所对的角度为K = 360-90*2-X = 180-X
  5. 假设两个切点为(x1,y)和(x2,y)
  6. 连接这两点的弦长为l = (x2-x1)
  7. 在圆内,弦和两条长度为r(半径)的法线形成等腰三角形
  8. 垂线将三角形分成两个相等的直角三角形。
  9. 其中一个角为K/2,边长为l/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值,从中绘制弧线。这些值不应太远或靠近原点。


谢谢您花时间解释,但我几乎不理解您的方法以及如何实现它... - ZouBi
尝试想象你的角落顶点位于原点,并且朝向正y轴的直线,其中Y轴将它们之间的角度平分。 - Vikram Bhat
抱歉,我无法没有图片进行解释,但我会尝试添加一张图片。 - Vikram Bhat
如果你理解了这个解决方案,它就是常数时间的。此外,你可以旋转和平移其他顶点并执行步骤,然后再反转平移和旋转。 - Vikram Bhat

0

这与其他答案有点偏离,但也更简单。

我们将处理很多角度,因此需要使用许多arctan2来计算带公差的角度,因此要知道如何使用它。你还可以通过行列式来计算,因为在0附近,这些线条几乎是平行的。

找到想要简化的角度,比如任何大于某个角度阈值的角度。执行以下三个操作:

  1. 分割:将连接该角度的两个线段划分为任意50个小线段。您还可以只对接近锐角的部分进行分割。
  2. 平滑:重复应用平滑以控制您所需的钝化量。取每个系列的3个点。并将中间点移动到第一个点和最后一个点的中点处。注意:更接近意味着而不是。因此,其中amount是可调节的:amount *(p1-p2)+ p2。
  3. 简化:查找任何角度未超过某个阈值的点,比如说1%,然后继续删除那些点(您还可以检查中点与中心点之间的偏差)。这通常会撤消分割,但我们已经对其进行了平滑处理,并且原始的锐角已经扩散成曲线。

你可以注意到,应用平滑的次数将仅传播那么多线段的角度。因此,如果您将其分段为50个部分。如果您只应用平滑10次,则仅最靠近拐角的10条线可能具有修改后的角度。

所以你只需要选择一些值,你要将多少行分段?你要应用多少平滑?你要应用平滑多少次?当你简化回来时,多大的偏差足以保留角度。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接