0

I am interested to learn why, for the following square hollow section (SHS), colour bleeds beyond the inside and outside shape boundaries at the location of its rounded corners. When the the number of coordinates calculated to create a corner curve is 90 (i.e. for each degree of arc), bleeding is barely visible, but for a value of 120 or more, bleeding is obvious.

Please can someone explain why this occurs?

To recreate the issue, create a Form not less than 1000 x 1000px in size with a button located on it, then vary the value for the 'AngularIncrements' variable in the Button1_Click Subroutine, which defines how smooth (faceted) a corner curve is, but also affects the magnitude of bleeding. I also find that an increased value for 't', which controls shape wall thickness, correspondingly increases bleeding.

Note that the shape's yellow infill is a DrawPolygon line of user-defined thickness, not a FillPolygon infill. I appreciate that one way around my issue is to:

  1. Paint a solid shape using FillPolygon infill, then;
  2. Paint on top of it a 'hole' in the Form's BackColor to create the illusion of a hollow shape, then;
  3. Paint the inside boundary line, then;
  4. Paint the outside boundary line.

However, this leads to a visible flicker, I think because VB takes a perceptible split-second for the solid area of point 2 to be painted over that for point 1. Paining a line of specified thickness for wall thickness eliminates this, but leads to bleeding as noted above.

Public Class Form1

Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    ' Define square hollow section (SHS) dimensions.
    Dim D As Decimal = 500 ' Depth.
    Dim B As Decimal = D ' Breadth.
    Dim t As Decimal = 50 ' Wall thickness.
    Dim ro As Decimal = 2 * t ' Outside radius.
    Dim ri As Decimal = t ' Inside radius.

    ' Define number of increments around quarter-curves.
    Dim AngularIncrements As Integer = 120

    ' Calculate SHS coordinates.
    Dim sectionCoords(-1)() As Decimal
    sectionCoords = Calculate_SHS_Coordinates(AngularIncrements, D, B, ro, ri)

    ' Paint SHS.
    Paint_SHS(sectionCoords, t)
End Sub

Private Function Calculate_SHS_Coordinates(ByVal AngularIncrements As Integer, ByVal D As Decimal, ByVal B As Decimal, ByVal ro As Decimal, ByVal ri As Decimal)
    Dim ϴ As Decimal = Math.PI / 2
    Dim IncrementAngle As Decimal = ϴ / AngularIncrements

    ' Calculate radii coordinates.
    Dim rx As Decimal = -B / 2 + ro
    Dim ry As Decimal = -D / 2 + ro

    ' Calculate quarter-section coordinates.
    Dim inputCoordsOuter(2 * AngularIncrements + 1) As Decimal
    Dim inputCoordsInner(2 * AngularIncrements + 1) As Decimal

    For i As Integer = 0 To 2 * AngularIncrements Step 2
        inputCoordsOuter(i) = rx - ro * Math.Sin(i / 2 * IncrementAngle)
        inputCoordsOuter(i + 1) = ry - ro * Math.Cos(i / 2 * IncrementAngle)

        inputCoordsInner(i) = rx - ri * Math.Sin(i / 2 * IncrementAngle)
        inputCoordsInner(i + 1) = ry - ri * Math.Cos(i / 2 * IncrementAngle)
    Next

    ' Mirror quarter-section to calculate half-section coordinates.
    ReDim Preserve inputCoordsOuter(2 * inputCoordsOuter.Length - 1)
    ReDim Preserve inputCoordsInner(2 * inputCoordsInner.Length - 1)

    For i As Integer = inputCoordsOuter.Length - 2 To inputCoordsOuter.Length / 2 Step -2
        inputCoordsOuter(i) = inputCoordsOuter(inputCoordsOuter.Length - (i + 2))
        inputCoordsOuter(i + 1) = -inputCoordsOuter(inputCoordsOuter.Length - (i + 1))

        inputCoordsInner(i) = inputCoordsInner(inputCoordsInner.Length - (i + 2))
        inputCoordsInner(i + 1) = -inputCoordsInner(inputCoordsInner.Length - (i + 1))
    Next

    ' Mirror half-section to calculate full-section coordinates.
    ReDim Preserve inputCoordsOuter(2 * inputCoordsOuter.Length - 1)
    ReDim Preserve inputCoordsInner(2 * inputCoordsInner.Length - 1)

    For i As Integer = inputCoordsOuter.Length - 2 To inputCoordsOuter.Length / 2 Step -2
        inputCoordsOuter(i) = -inputCoordsOuter(inputCoordsOuter.Length - (i + 2))
        inputCoordsOuter(i + 1) = inputCoordsOuter(inputCoordsOuter.Length - (i + 1))

        inputCoordsInner(i) = -inputCoordsInner(inputCoordsInner.Length - (i + 2))
        inputCoordsInner(i + 1) = inputCoordsInner(inputCoordsInner.Length - (i + 1))
    Next

    ' Return to first coordinate pair.
    ReDim Preserve inputCoordsOuter(inputCoordsOuter.Length + 1)
    inputCoordsOuter(inputCoordsOuter.Length - 2) = inputCoordsOuter(0)
    inputCoordsOuter(inputCoordsOuter.Length - 1) = inputCoordsOuter(1)

    ReDim Preserve inputCoordsInner(inputCoordsInner.Length + 1)
    inputCoordsInner(inputCoordsInner.Length - 2) = inputCoordsInner(0)
    inputCoordsInner(inputCoordsInner.Length - 1) = inputCoordsInner(1)

    ' Place outer and inner arrays in jagged array.
    Dim inputCoords()() = {inputCoordsOuter, inputCoordsInner}

    Return inputCoords
End Function

Private Sub Paint_SHS(ByVal sectionCoords()() As Decimal, ByVal t As Decimal)
    ' Define the SHS centre location.
    Dim originX As Integer = Me.Width / 2
    Dim originY As Integer = Me.Height / 2

    ' Define paint colours.
    Dim infillColour As Color = Color.Yellow
    Dim inlineOutlineColour As Color = Color.Black
    Dim lineWidth As Integer = 1

    ' Define form as graphics object.
    Dim Canvas As Graphics = Me.CreateGraphics

    ' Calculate coordinates for centre of wall thickness, and paint (thick) line.
    Dim paintCoords(sectionCoords(0).Length / 2 - 1) As Point

    For i As Integer = 0 To sectionCoords(0).Length - 1 Step 2
        paintCoords(i / 2) = New Point(originX + (sectionCoords(0)(i) + sectionCoords(1)(i)) / 2, originY - (sectionCoords(0)(i + 1) + sectionCoords(1)(i + 1)) / 2)
    Next
    Paint_Line(Canvas, paintCoords, infillColour, t)

    ' Calculate outline coordinates and paint outline.
    For i As Integer = 0 To sectionCoords(0).Length - 1 Step 2
        paintCoords(i / 2) = New Point(originX + sectionCoords(0)(i), originY - sectionCoords(0)(i + 1))
    Next
    Paint_Line(Canvas, paintCoords, inlineOutlineColour, lineWidth)

    ' Calculate inline coordinates and paint inline.
    For i As Integer = 0 To sectionCoords(1).Length - 1 Step 2
        paintCoords(i / 2) = New Point(originX + sectionCoords(1)(i), originY - sectionCoords(1)(i + 1))
    Next
    Paint_Line(Canvas, paintCoords, inlineOutlineColour, lineWidth)
End Sub

Private Sub Paint_Line(ByVal Canvas As Graphics, ByVal paintCoords As Point(), ByVal paintColour As Color, ByVal lineWidth As Decimal)
    Dim outlinePen = New Pen(paintColour, CSng(lineWidth))
    Canvas.DrawPolygon(outlinePen, paintCoords)
    outlinePen.Dispose()
End Sub
End Class

EDIT: I've rewritten the example to simplify it, address issues noted below in the comments and to increase legibility. However, I still am unable to paint the shape to the screen correctly, as follows:

  1. For a small AngularIncrements value, which defines the number of facets around a 90 degree curve, the shape appears to be painted correctly, but some increment values (e.g. 150) result in a clearly visible error.
  2. As the value for AngularIncrements increases, the width of the shape's wall, which should be constant everywhere, varies over the 90 degree curve length, being thickest at its mid-length. I could understand this reducing as the number of facets increases, but not the converse. Try an AngularIncrements value of 1500 to see what I mean. Now, one could argue that this number of angular increments is overkill, and I'd agree, but I'm interested to learn what is causing this issue.

Here's the revised code:

Imports System.Drawing.Drawing2D

Public Class Form1
Private yesPaint As Boolean = False

Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    ' Paint shape.
    yesPaint = True
    Me.Invalidate()
End Sub

Private Sub Form1_Paint(sender As Object, e As PaintEventArgs) Handles Me.Paint
    If yesPaint = True Then
        ' Define shape.
        Dim D As Decimal = 450
        Dim B As Decimal = D
        Dim t As Decimal = 100
        Dim r As Decimal = 100
        Dim AngularIncrements As Integer = 150

        ' Calculate coordinates for painting by converting to integer values the set of coordinates used for engineering property calculations (not included in this example).
        ' Coordinates represent the centre of shape wall thickness.
        Dim paintCoords As Point() = Calculate_SHS_Coordinates(AngularIncrements, D, B, r)

        ' Calculate screen origin coordinates.
        Dim originX As Integer = CInt(Me.Width / 2)
        Dim originY As Integer = CInt(Me.Height / 2)

        ' Recalculate paint coordinates relative to screen origin.
        For i As Integer = 0 To paintCoords.Length - 1
            paintCoords(i).X = paintCoords(i).X + originX
            paintCoords(i).Y = paintCoords(i).Y + originY
        Next

        ' Create graphics path object and add polygon.
        Dim SHSpath = New GraphicsPath()
        SHSpath.AddPolygon(paintCoords)

        ' Create pen.
        Dim blackPen = New Pen(Color.Black, t)

        ' Paint graphics path.
        e.Graphics.SmoothingMode = SmoothingMode.AntiAlias
        e.Graphics.DrawPath(blackPen, SHSpath)
        blackPen.Dispose()

        yesPaint = False
    End If
End Sub

Private Function Calculate_SHS_Coordinates(ByVal AngularIncrements As Integer, ByVal D As Decimal, ByVal B As Decimal, ByVal r As Decimal) As Point()
    Dim ϴ As Double = Math.PI / 2
    Dim IncrementAngle As Decimal = CDec(ϴ) / AngularIncrements

    ' Calculate quarter-section radius coordinates.
    Dim rx As Decimal = -B / 2 + r
    Dim ry As Decimal = -D / 2 + r

    ' Calculate quarter-section coordinates.
    Dim inputCoords(AngularIncrements) As Point
    For i As Integer = 0 To AngularIncrements
        inputCoords(i).X = CInt(rx - r * Math.Sin(CDec(i) * IncrementAngle))
        inputCoords(i).Y = CInt(ry - r * Math.Cos(CDec(i) * IncrementAngle))
    Next

    ' Mirror quarter-section to calculate half-section coordinates.
    ReDim Preserve inputCoords(2 * inputCoords.Length - 1)
    For i As Integer = inputCoords.Length - 1 To CInt(inputCoords.Length / 2) Step -1
        inputCoords(i).X = inputCoords(inputCoords.Length - (i + 1)).X
        inputCoords(i).Y = -inputCoords(inputCoords.Length - (i + 1)).Y
    Next

    ' Mirror half-section to calculate full-section coordinates.
    ReDim Preserve inputCoords(2 * inputCoords.Length - 1)
    For i As Integer = inputCoords.Length - 1 To CInt(inputCoords.Length / 2) Step -1
        inputCoords(i).X = -inputCoords(inputCoords.Length - (i + 1)).X
        inputCoords(i).Y = inputCoords(inputCoords.Length - (i + 1)).Y
    Next

    ' Return to first coordinate pair.
    ReDim Preserve inputCoords(inputCoords.Length)
    inputCoords(inputCoords.Length - 1).X = inputCoords(0).X
    inputCoords(inputCoords.Length - 1).Y = inputCoords(0).Y

    Return inputCoords
End Function
End Class
  • 1
    Set Option Strict On, Option Explicit On, use `Single` instead of `Decimal` (then fix all the issues that pop up - since now you can see them), use [GraphicsPath.AddPolygon()](https://learn.microsoft.com/en-us/dotnet/api/system.drawing.drawing2d.graphicspath.addpolygon) + `Graphics.DrawPath()` or `Graphics.FillPath()`, ► **don't use `Control.CreateGraphics()` anymore**, don't use `Redim Preserve`, use `List`, set the Form's DoubleBuffer = true. – Jimi May 17 '20 at 03:51
  • A few samples that show how to draw Arcs: [Here](https://stackoverflow.com/a/54139910/7444103), [Here](https://stackoverflow.com/a/54794097/7444103), [Here](https://stackoverflow.com/a/56533229/7444103). With Polygons, [Here](https://stackoverflow.com/a/52921415/7444103). Never mind the language, read the notes. Even though it's C#, it's the same thing in VB.Net. – Jimi May 17 '20 at 04:01
  • @Jimi - Option Strict was already set to On but Option Explicit was not. It is now, but this revision has not resulted in the flood of VB warnings and/or Errors your comment seems to imply will occur. EDIT: Ahh, I see now that the revision applied only to new projects. After revising the setting for the current project I see your point. Going forward I’ll be sure to adopt what I assume is considered good practice – making explicit any type conversion – but with regard to the issue described in the OP I’m not sure it affects the result? – Rob Buckle May 17 '20 at 09:22
  • Changing from the Decimal to Single data type will reduce the accuracy of coordinates calculated in the Calculate_SHS_Coordinates Subroutine. The former was deliberately selected by me for its accuracy, despite the associated overhead, because the above code is part of a program that calculates engineering properties for structural sections (shapes), for which accuracy of numerical results is important. Painting the shape afterward is merely a visual nice-to-have, and makes use of the same coordinate set to calculate paint (Point) coordinates simply because it seemed to me efficient to do so. – Rob Buckle May 17 '20 at 09:22
  • Using GraphicsPath.AddPolygon() + Graphics.DrawPath() has made no visual difference, sorry. I’ve read the links and associated comments you provided, thanks, but unfortunately I’m not sure what I’m missing. Separately, although not included in the OP, I’m aware of SmoothingMode.AntiAlias, but the bleeding is too severe for this to make a significant difference. – Rob Buckle May 17 '20 at 09:22
  • I was unaware of List and will investigate how to incorporate it. An initial read indicates that it’s a generic array that is dynamically increased/decreased, which I guess was your point regarding eliminating my use of Redim Preserve. Apparently it’s also inherently faster than ‘traditional’ Arrays, which is of especial interest to me. Thanks for this tip. – Rob Buckle May 17 '20 at 09:23
  • I was also unaware of the DoubleBuffered property. My understanding is this won’t change my current issue, but would eliminate visual flickering if painting was accomplished via the method described in my numbered list. This may be handy in future, so thanks again. – Rob Buckle May 17 '20 at 09:23
  • Decimal is not used when drawing. You have to use Single (`float`) values. Some methods only accept integer values. If you need to perform calculations, do it elsewhere (when you produce these value), when you draw, you use `float` values. -- How `GraphicsPath.AddPolygon()` and other GraphicsPath method work it's something you have to learn for yourself. You cannot have *Color bleedings* using a GraphicsPath: if you do, then you're not using his class, or the Graphics class and methods, as you're supposed to. Take your time and take a look at the code I linked. It's all there. – Jimi May 17 '20 at 09:55
  • @Jimi - I know Decimal isn't used for drawing, but I assumed that the Decimal data type coordinates calculated in the Calculate_SHS_Coordinates Function was (implicitly) converted to the Single data type (and hence truncated in accuracy) when the Point coordinates, which are intentionally a separate coordinate set used to paint, are calculated in the Paint_SHS Subroutine. But, perhaps not. I'll reinvestigate and come back to you, taking my time as you advise. Thank you! – Rob Buckle May 17 '20 at 19:43
  • @Jimi - Sorry for the delayed response -- I've been flat out at work until now. Please refer to the edited OP. I've simplified the example and tidied up the code. Coordinates are calculated accurately (Decimal type) but converted to Points (Integer type) at the final stage. I've left off revising my use of Redim Preserve and List for now. I'm no longer using Control.CreateGraphics() anymore, but as you can see I'm still at a loss to explain what I'm doing wrong. Any advice would be very welcome... – Rob Buckle May 22 '20 at 08:34
  • I've already told you to use float values. Change `Dim paintCoords As PointF()` and of course `Private Function Calculate_SHS_Coordinates(...) As PointF()`. Then, of course, `Dim ϴ As Single = Math.PI / 2 Dim IncrementAngle As Single = ϴ / AngularIncrements inputCoords(i).X = CSng(rx - r * Math.Sin(i * IncrementAngle)) inputCoords(i).Y = CSng(ry - r * Math.Cos(i * IncrementAngle))`. Then, in `Form_Paint`, of course, `Dim originX As Single = CSng(Me.Width / 2) Dim originY As Single = CSng(Me.Height / 2)`. – Jimi May 22 '20 at 09:03
  • Now, do you want to use Decimal (the slowest data structure ever, never - ever - used in Graphics procedures, also because subject to constant conversions and rounding, since no drawing method accepts Decimal values) to make these calculation without any advantage whatsoever? Be my guest. Also, this is not really the way to generate arcs. Already mentioned this, too. – Jimi May 22 '20 at 09:03
  • @Jimi - I understand that it must seem like I’ve ignored your advice re the Decimal data type, but this is not correct. As mentioned previously, I'm aware that painting does not use the Decimal data type. If all I wished to do was calculate coordinates for painting then I'd use the Single data type from the outset. – Rob Buckle May 22 '20 at 11:39
  • A precise coordinate set is required regardless of painting because the actual program calculates engineering properties using polygon vertices values, including for a user-defined irregular polygon in which curves are formed from an arbitrary number of facets, which I trust explains why curves in my example are intentionally formed from facets and not arcs. – Rob Buckle May 22 '20 at 11:39
  • Painting needs to reuse the above-mentioned, mandatory, coordinate set, but I’m aware the data type needs to be revised for this to occur, hence why in the example the coordinates, after assumed to have been used elsewhere, are converted to integer data type values. I assumed that integer values would suffice because pixel coordinates obviously cannot hold a fractional value. – Rob Buckle May 22 '20 at 11:39
  • I understand that my example may make it seem like I’m wastefully calculating Decimal data type coordinates, only to convert them to integer data type values for painting, but there is a rational reason for doing so, even if it makes no apparent sense to others. – Rob Buckle May 22 '20 at 11:40
  • I made the changes you suggest and confirm that no matter the number of facets, the curves now paint correctly. I apologise for apparently frustrating you, but for what it’s worth, I’m very grateful for your help. Thanks again. – Rob Buckle May 22 '20 at 11:40
  • No problem, I'm glad it was helpful. Btw, you cannot *frustrate* me, I have no obligation to answer. If I do, it's because I choose to. – Jimi May 22 '20 at 14:54

0 Answers0