Click here to Skip to main content
15,867,568 members
Articles / Multimedia / GDI+
Article

CNC Graphical Backplotter

Rate me:
Please Sign up or sign in to vote.
4.89/5 (47 votes)
30 Nov 2008CPOL8 min read 262.8K   19.3K   117   59
Article and source code for creating a CNC graphical backplotter
Sample image

Introduction

This program will process and graphically display CNC code. This plain text code is used by machine tools to cut metal and other materials. Wikipedia has information on CNC if you are not familiar with it. A CNC Programmer would use this type of program to verify code (G-code) before sending it to a machine to cut a part.

Background

My big plan was to simply use the Upgrade Wizard on an old CNC viewer program that I had written in VB6 and then "Give it away give it away give it away now" when I was finished. It ended up being an exercise in GDI+ and a total rewrite of the parser using Regular Expressions. It's part of a larger project that I am currently rewriting in VB.NET 2005.

These were my requirements for this project.

  • Faster G-code parsing. Regular Expressions helps here.
  • Multiple viewports. 1,2 and 4. A custom User Control was key.
  • 3-D view manipulation. A nimble display list is created with a geometry transformation.
  • Tool filtering to hide and show specific tools.
  • Graphical selection or picking. Combination of a back-buffer and real-time drawing was the answer.

Using the Code

Parsing

G-code is fairly simple. It's a plain ASCII text file made up of lines or "blocks" as they are sometimes called. Each line has a number of words that begin with a letter and is always followed by a number. For example; G00 X1.0. CNC programs can have callable procedures as well. They are commonly used for a set of hole locations that will be used for a number of tools. These procedures or "sub programs" have a specific beginning and end label.

Regular expressions seemed perfect for the job of breaking the file into programs, breaking the programs into blocks and then finally breaking the blocks into individual words. The .NET implementation of Regular Expressions also has an option to be compiled. This helps us get the best performance if the match expression does not change very often.
Because it's not unusual for some CNC programs to exceed 100,000 lines of code, performance is important.

Regular Expressions helped me get the time down from "Go get a coffee" to about 10 seconds on my 97,000-line dataset.

The first match expression that matches programs looks like this. [:\$O]+([0-9]+) It identifies a program label by matching a :, a$ or the Letter O. The \ is needed to escape the $ so RegEx doesn't get confused. The string is concatenated at run time using labels that are specific to each machine tool as shown.

C#
mRegSubs = New Regex(comment & "[" & progId & "]([0-9]+)", 
                     RegexOptions.Compiled) 

As you can see you simply add RegexOptions.Compiled as an optional parameter.
Because g-code supports human readable comments within the code a little extra work is needed.

Comments are usually between parentheses. For example. (REMOVE CLAMP) The comments can also span multiple lines. So the expression match needed to distinguish between comments and actual G-code words. Not a big deal. I used Expresso to help me nail it down.

This match is a little more complicated and looks like this.

\([^\(\)]*\)|[/\*].*\n|\n|[A-Z][-+]?[0-9]*\.?[0-9]*

The first part of the match will match a comment or a newline. The second part will match a letter followed by a number.
In code, the match string is constructed based on what the user specifies as a comment character.

VB.NET
Dim progId As String = Regex.Escape(mCurMachine.ProgramId) 
Dim comment As String = comments(0) & "[^" & comments(0) &_
                        comments(1) & "]*" & comments(1) & "|" 
 
mRegWords = New Regex(comment & "[" & skipChars & "].*\n|\n|" &_
                      My.Resources.datRegexNcWords, _
                      RegexOptions.Compiled Or RegexOptions.IgnoreCase) 

Once the match strings are set we parse the file.

VB.NET
For Each m As Match In Me.mRegSubs.Matches(sFileContents)
    ...
Next

Each program match is then processed for word matching.

VB.NET
For Each p In mNcProgs
    mTotalBites = p.Contents.Length
    ProcessSubWords(p)
Next

Matches are either a newline, a comment or a word.

VB.NET
Private Sub ProcessSubWords(ByVal p As clsProg)
    For Each ncWord As Match In Me.mRegWords.Matches(p.Contents)
        'Each word
        If ncWord.Value = vbLf Then 'Is this a newline
            CreateGcodeBlock()
        ElseIf MatchIsComment(ncWord) Then
            'Comment
            mTotalLines += ncWord.Value.Split(CChar(vbLf)).Length - 1
        ElseIf mCurMachine.BlockSkip.Contains(ncWord.Value.Chars(0)) Then
            'Blockskip.
            mTotalLines += 1
        Else
            'Word
             EvaluateWord()
        End If
    Next
End Sub

Then we evaluate each word appropriately based on the letter or label value.

VB.NET
Private Sub EvaluateWord()
    Select Case mCurAddress.Label
        Case "X"c
            mXpos = FormatAxis(mCurAddress.StringValue,
                               mCurMachine.Precision)
        Case "Y"c
            mYpos = FormatAxis(mCurAddress.StringValue,
                               mCurMachine.Precision)
        Case "Z"c
        ...

When a newline is matched we add a record to a collection. This is similar to the way the CNC machine will process the g-code. This collection will be shared amongst all the instances of the viewer control and will be used to create viewer-specific display lists.

VB.NET
'Results of the parsing stored here 
Public Shared MotionBlocks As New List(Of clsMotionRecord) 
'Each viewer control will have it's own version of this list 
Private mDisplayLists As New List(Of clsDisplayList) 

Processing and Rendering

GDI+ is quite rich but as the namespace System.Drawing.Drawing2D suggests its 2D only. It will draw just about anything and allow you to transform it using a 3 x 3 matrix. The approach I used was to create a master list (MotionBlocks) that is shared among all the viewports. Then create a slimmer display list (clsDisplayList)that has had the coordinates rotated in 3-D space.

This transformation is done any time the view orientation is changed. The 3-D rotation code is not fancy but it gets the job done. Look for Private Sub DrawEachElmt() in MG_BasicViewer.vb for details.

The way that machine tools actually move when in rapid positioning mode is also considered. if the machine is at X0,Y0,Z0 and rapids to X1.0 Y2.0 Z3.0 it does not actually move in one linear path. It will move in a dog-leg type motion by completing the shortest axis distance first.

If you download and play with the program you will also notice that it displays a coordinate system indicator at X,Y,Z zero, and that it never changes size. This is done by using an exclusive matrix for the coordinate system that maintains a constant scale factor.

Arcs and circles are another thing that GDI+ will draw just fine but if you need to rotate the arc in 3D space you're out of luck. All the arcs and circles in this program are drawn using line segments that resemble arcs. See the PolyCircle sub for details. You would think this would be wicked slow but it works fine and actually has a couple of advantages.

One of them is that a helix is easy to do if you draw it as line segments. CNC machines often cut a circle while moving in the Z axis to form a helix. Milling screw threads is an example of this.

The other advantage is that you have control over the quality of the arc by drawing more or less line legments.

If you have a large number of arcs that are small on the screen you can draw them with a small number of segments to save rendering time.

The fact that everything is drawn as a line segment also helps when we need to find the extents of the geometry at a particular view orientation. We just rip through all the end points in the display list and set the max and min values.

VB.NET
For Each l As clsDisplayList In mDisplayLists
    For Each p As PointF In l.Points
        mExtentX(0) = Math.Min(mExtentX(0), p.X)
        mExtentX(1) = Math.Max(mExtentX(1), p.X)
        mExtentY(0) = Math.Min(mExtentY(0), p.Y)
        mExtentY(1) = Math.Max(mExtentY(1), p.Y)
    Next
Next

Hit Testing

Selection was another important feature I wanted. Basically, if the mouse is near enough to a line that we have previously drawn, it should be identified by drawing it in a thicker pen. The following is a fragment from the custom rectangle class that can determine if a line passes through it. I was pleasantly surprised with the performance of the code.

VB.NET
Public Function Contains(ByVal x As Single, ByVal y As Single) _
                                                          As Boolean
    Return x > Left And x < Right And y > Bottom And y < Top
End Function

Public Function IntersectsLine(ByVal x1 As Single, ByVal y1 As Single,_
                   ByVal x2 As Single, ByVal y2 As Single) As Boolean
    'Trivial test inside
    If Me.Contains(x1, y1) Or Me.Contains(x2, y2) Then
        Return True
    End If
    'Trivial test outside
    If x1 < Me.Left And x2 < Me.Left Then
        Return False
    ElseIf x1 > Me.Right And x2 > Me.Right Then
        Return False
    ElseIf y1 < Me.Bottom And y2 < Me.Bottom Then
        Return False
    ElseIf y1 > Me.Top And y2 > Me.Top Then
        Return False
    End If

    'Trivial test vertical or horizontal
    If x1 = x2 Then
        Return True
    End If
    If y1 = y2 Then
        Return True
    End If

    Dim slope As Single = (y2 - y1) / (x2 - x1)
    Dim Yintercept As Single = y1 - (slope * x1)
    Dim iptX As Single
    Dim iptY As Single

    'Left edge
    iptX = Me.Left
    iptY = (slope * iptX) + Yintercept
    If iptY > Me.Bottom And iptY < Me.Top Then
        Return True
    End If

    'Right edge
    iptX = Me.Right
    If iptY > Me.Bottom And iptY < Me.Top Then
        Return True
    End If

    'Top edge
    iptY = Me.Top
    iptX = ((iptY - Yintercept) / slope)
    If iptX > Me.Left And iptX < Me.Right Then
        Return True
    End If

    'Bottom edge
    iptY = Me.Bottom
    iptX = ((iptY - Yintercept) / slope)
    If iptX > Me.Left And iptX < Me.Right Then
        Return True
    End If
    Return False
End Function

Rendering

Once the hit-test code was written things looked pretty good. On a small CNC program that is. It's always good to test your program on large datasets periodically. And this test failed badly. If the entire display list is enumerated every time the mouse is moved things slow down. All we need to do is just draw a few lines in a thicker pen.

The solution was to first draw all the graphics to a buffer, and then in the Paint event of the control simply draw that buffer, which is very fast. Then draw any selected geometry in real-time on top of the buffer.

When the mouse is in selection mode, the graphics never change position so there is no need to re-create the entire display list. So as the mouse moves we draw only the items in the display list that are in the hit zone after we draw the buffer.

DrawSelectionOverlay is simply called from the MouseMove event when in selection mode. The BufferedGraphics class was used to implement my custom back buffer.

VB.NET
 Private Sub SetBufferContext()
    If mGfxBuff IsNot Nothing Then
        mGfxBuff.Dispose()
        mGfxBuff = Nothing
    End If
    ' Retrieves the BufferedGraphicsContext for the current application 
    ' domain.
    mContext = BufferedGraphicsManager.Current
    ' Sets the maximum size for the primary graphics buffer
    mContext.MaximumBuffer = New Size(Me.Width + 1, Me.Height + 1)
    ' Allocates a graphics buffer the size of this control
    mGfxBuff = mContext.Allocate(CreateGraphics(), 
                                 New Rectangle(0, 0, Me.Width, Me.Height))
    mGfx = mGfxBuff.Graphics
End Sub        
        
 Private Sub DrawSelectionOverlay()
    'Draw the buffer
    mGfxBuff.Render()
    
    'Draw the selection overlay.
    mCurPen.Width = ((1 / mDpiX) / mScaleToReal) * 2
    With Graphics.FromHwnd(Me.Handle)
        .PageUnit = GraphicsUnit.Inch
        .ResetTransform()
        .MultiplyTransform(mMtxDraw)
        For Each p As clsDisplayList In mSelectionHitLists
            mCurPen.Color = p.Color
            If p.Rapid Then
                mCurPen.DashStyle = Drawing2D.DashStyle.Dash
            Else
                mCurPen.DashStyle = Drawing2D.DashStyle.Solid
            End If
            .DrawLines(mCurPen, p.Points)
        Next
    End With
End Sub

Points of Interest

The code that does the 3D transformations is old and certainly OpenGL and DirectX are better solutions. Subsequently, I didn't spend any time optimizing it. The whole project was done in "Git-r-done" mode because a more advanced viewer is currently being written using OpenGL and C++.

The selection performance was a big surprise. When there are so many lines on the screen that you can barely see the background color and it's fast, I'm happy. I thought this level of performance was only possible using OpenGL.

Another thing I ran into that I have seen before is a condition where a line does not render completely. A gap can be seen. This will happen when the end points of the line are way off the screen and the line is at a very slight angle. I ended up testing for this condition and making corrections for it. If anyone knows why this happens and how to prevent it please let me know.

You will find some other tricks in the code to boost performance. For example, a test is done to determine if the line is completely off the screen. If it is then it is not drawn into the buffer. It takes way less time to do the test than to draw into the buffer. So the closer you zoom into a large dataset the faster the view manipulation gets.

Also the System.Drawing.Drawing2D.GraphicsPath class looked like it was made for this type of project but it proved to be slow on large datasets. As you can see in the above fragment, the DrawLines method which accepts an array of points was my choice.

You won't find too much error handling in his demo. I stripped it out to keep it simple. I also removed the entire machine configuration form.

History

This is my second article on The Code Project and I hope it's of some value to others.
And of course I welcome suggestions and criticisms.

  • 31st January, 2007: Initial post
  • 28th November, 2008: Source code updated to include C#

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer
United States United States
Currently working as a CAD(Computer aided design) trainer and macro developer for a CAD company. Background also includes 8 years of CNC(computer numerical control) programming and machine tool automation.

Comments and Discussions

 
QuestionVisual Studio 2022 Pin
Fabcomachine24-Dec-22 19:28
Fabcomachine24-Dec-22 19:28 
QuestionGenerate cnc code Pin
Parthasarathy Mandayam4-May-22 18:03
Parthasarathy Mandayam4-May-22 18:03 
QuestionAspect ratio/distortion question Pin
Member 119509591-Feb-20 19:04
Member 119509591-Feb-20 19:04 
Questionv1.0.0.31 source code Pin
Member 1234071227-Feb-16 2:37
Member 1234071227-Feb-16 2:37 
QuestionLatest C# version Pin
BarretteBaron22-Apr-15 10:25
BarretteBaron22-Apr-15 10:25 
QuestionWhat is full form of "Wcs" in source code? Pin
aruyc2-Jan-15 0:55
aruyc2-Jan-15 0:55 
QuestionTool Library Pin
patch.wheatley11-Apr-13 6:27
patch.wheatley11-Apr-13 6:27 
GeneralMy vote of 5 Pin
Manoj Kumar Choubey22-Feb-12 21:42
professionalManoj Kumar Choubey22-Feb-12 21:42 
QuestionConversational Cnc Programming System Pin
c0g06-Feb-12 8:04
c0g06-Feb-12 8:04 
AnswerRe: Conversational Cnc Programming System Pin
Jason Titcomb7-Feb-12 13:07
Jason Titcomb7-Feb-12 13:07 
GeneralRe: Conversational Cnc Programming System Pin
c0g08-Feb-12 1:13
c0g08-Feb-12 1:13 
GeneralHelp on the code Pin
betselot hailu12-Jan-12 21:07
betselot hailu12-Jan-12 21:07 
GeneralMy vote of 5 Pin
Jon Gunnar Brekke7-Dec-11 11:54
Jon Gunnar Brekke7-Dec-11 11:54 
Questionneed references and links to guide me in my project !! Pin
sessem133-Oct-11 9:41
sessem133-Oct-11 9:41 
QuestionC# Code Pin
erkankocakaya16-Jul-11 7:16
erkankocakaya16-Jul-11 7:16 
AnswerRe: C# Code Pin
Daniel Dumitru19-Sep-11 23:24
Daniel Dumitru19-Sep-11 23:24 
GeneralC# code Pin
steve234911-Feb-11 2:15
steve234911-Feb-11 2:15 
GeneralMy vote of 5 Pin
Binary Eskimo24-Oct-10 6:47
professionalBinary Eskimo24-Oct-10 6:47 
GeneralC# code Pin
ilpulcinocalimero14-Oct-10 5:54
ilpulcinocalimero14-Oct-10 5:54 
Generallatest c# code Pin
Mischel6-Oct-10 2:35
Mischel6-Oct-10 2:35 
GeneralRe: latest c# code Pin
Daniel Dumitru8-Dec-10 23:08
Daniel Dumitru8-Dec-10 23:08 
GeneralG code in C# Pin
OneManDo19-Aug-10 2:35
OneManDo19-Aug-10 2:35 
GeneralV1.0.0.18 Pin
noam shapira5-Mar-10 5:49
noam shapira5-Mar-10 5:49 
GeneralV1.0.0.17 Pin
overraider29-Sep-09 7:31
overraider29-Sep-09 7:31 
You can send me source code from V1.0.0.17 ?
GeneralRe: V1.0.0.17 Pin
DaDoo7017-Feb-10 5:20
DaDoo7017-Feb-10 5:20 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.