65.9K
CodeProject 正在变化。 阅读更多。
Home

CNC 图形后处理程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (46投票s)

2007年1月31日

CPOL

8分钟阅读

viewsIcon

272506

downloadIcon

19552

创建 CNC 图形后处理程序的文章和源代码。

Sample image

引言

此程序将处理并图形化显示 CNC 代码。这种纯文本代码由机床用于切割金属和其他材料。如果您不熟悉 CNC,维基百科上有关于CNC的信息。CNC 程序员会使用此类程序来验证代码(G 代码),然后再将其发送到机器上切割零件。

背景

我最初的计划是直接使用升级向导处理一个我用 VB6 编写的旧 CNC 查看器程序,然后“全部免费赠送”。结果却变成了一次 GDI+ 的练习,以及使用正则表达式重写了整个解析器。这是我目前正在用 VB.NET 2005 重写的一个大型项目的一部分。

这些是我对该项目的要求。

  • 更快的 G 代码解析。正则表达式在此方面很有帮助。
  • 多个视口。1、2 和 4。自定义用户控件是关键。
  • 3D 视图操作。使用几何变换创建了一个灵活的显示列表。
  • 工具过滤以隐藏和显示特定工具。
  • 图形选择或拾取。后端缓冲区和实时绘制的组合是答案。

Using the Code

解析

G 代码相当简单。它是一个纯 ASCII 文本文件,由行组成,有时也称为“块”。每行包含多个单词,以字母开头,后面总是跟着一个数字。例如:G00 X1.0。CNC 程序还可以有可调用的子程序。它们通常用于一组将由多个工具使用的孔位。这些子程序或“子程序”具有特定的开始和结束标签。

正则表达式似乎是处理将文件分解为程序、程序分解为块,然后块分解为单个单词的完美解决方案。.NET 实现的正则表达式还有一个编译选项。这有助于我们获得最佳性能,前提是匹配表达式不经常更改。
由于一些 CNC 程序代码量可能超过 100,000 行,因此性能很重要。

正则表达式帮助我将处理 97,000 行数据集的时间从“去喝杯咖啡”减少到大约 10 秒。

第一个匹配程序的正则表达式如下:[:\$O]+([0-9]+)。它通过匹配冒号(:)、美元符号($)或字母 O 来识别程序标签。\ 用于转义 $,以免 RegEx 混淆。字符串在运行时使用特定于每台机床的标签进行连接,如下所示。

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

如您所见,只需将 RegexOptions.Compiled 添加为可选参数。
由于 G 代码支持代码中的人类可读注释,因此需要额外的工作。

注释通常在括号内。例如:(REMOVE CLAMP)。注释也可以跨越多行。因此,匹配表达式需要区分注释和实际的 G 代码单词。这不算什么大事。我使用了Expresso来帮助我精确地完成它。

这个匹配更复杂一些,如下所示。

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

匹配的第一部分将匹配一个注释或一个换行符。第二部分将匹配一个字母后跟一个数字。
在代码中,匹配字符串是根据用户指定的注释字符来构造的。

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) 

一旦设置了匹配字符串,我们就可以解析文件了。

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

然后处理每个程序匹配项以进行单词匹配。

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

匹配项是换行符、注释或单词。

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

然后,我们根据字母或标签值适当地评估每个单词。

    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
            ...

当匹配到换行符时,我们会向一个集合添加一个记录。这与 CNC 机床处理 G 代码的方式相似。此集合将在所有查看器控件实例之间共享,并用于创建特定于查看器的显示列表。

'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) 

处理和渲染

GDI+ 非常强大,但正如命名空间 System.Drawing.Drawing2D 所暗示的那样,它只支持 2D。它可以绘制几乎任何东西,并允许您使用 3x3 矩阵对其进行变换。我使用的方法是创建一个主列表(MotionBlocks),该列表由所有视口共享。然后创建一个更精简的显示列表(clsDisplayList),其中坐标已在 3D 空间中旋转。

每次更改视图方向时都会执行此变换。3D 旋转代码并不 fancy,但可以完成工作。有关详细信息,请查看 MG_BasicViewer.vb 中的 Private Sub DrawEachElmt()

还考虑了机床在快速定位模式下的实际移动方式。如果机器位于 X0,Y0,Z0 并快速移动到 X1.0 Y2.0 Z3.0,它实际上并不会沿直线路径移动。它会以一种“狗腿式”的运动方式移动,先完成最短轴的距离。

如果您下载并试用该程序,您还会注意到它在 X、Y、Z 零点显示一个坐标系指示器,并且它的大小永远不会改变。这是通过使用一个专用的坐标系矩阵来实现的,该矩阵保持恒定的比例因子。

GDI+ 可以很好地绘制圆弧和圆形,但如果您需要在 3D 空间中旋转圆弧,那就无能为力了。此程序中的所有圆弧和圆形都使用线段绘制,这些线段类似于圆弧。有关详细信息,请参阅 PolyCircle 子程序。您可能会认为这会非常慢,但实际上效果很好,而且还有一些优点。

其中一个优点是,如果将螺旋线绘制为线段,则很容易实现。CNC 机床通常在 Z 轴移动时切割圆形以形成螺旋线。铣削螺纹就是一个例子。

另一个优点是,您可以通过绘制更多或更少的线段来控制圆弧的质量。

如果您有大量在屏幕上很小的圆弧,您可以绘制更少的线段来节省渲染时间。

所有内容都绘制为线段这一事实在我们需要确定特定视图方向下的几何范围时也很有帮助。我们只需遍历显示列表中的所有端点,然后设置最大值和最小值。

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

命中检测

选择是我想要的另一个重要功能。基本上,如果鼠标足够靠近我们之前绘制的线条,就应该通过用更粗的画笔绘制它来识别它。下面是自定义矩形类的一个片段,它可以确定一条线是否穿过它。我对代码的性能感到惊喜。

    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

渲染

一旦编写了命中测试代码,一切看起来都很好。对于小型 CNC 程序来说是这样。定期用大型数据集测试您的程序总是好的。而这个测试惨败。如果每次移动鼠标时都遍历整个显示列表,速度就会变慢。我们只需要用更粗的画笔绘制几条线。

解决方案是首先将所有图形绘制到缓冲区,然后在控件的 Paint 事件中仅绘制该缓冲区,这非常快。然后实时在缓冲区之上绘制任何选定的几何图形。

当鼠标处于选择模式时,图形永远不会改变位置,因此无需重新创建整个显示列表。因此,随着鼠标的移动,我们在绘制缓冲区之后,仅绘制命中区域中的显示列表项。

DrawSelectionOverlay 只是在选择模式下的 MouseMove 事件中被调用。我使用了 BufferedGraphics 类来实现我的自定义后端缓冲区。

 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

关注点

执行 3D 变换的代码很老,OpenGL 和 DirectX 绝对是更好的解决方案。之后,我没有花时间优化它。整个项目都是在“赶工”模式下完成的,因为我正在使用 OpenGL 和 C++ 编写一个更高级的查看器。

选择性能是一个巨大的惊喜。当屏幕上有如此多的线条以至于几乎看不到背景颜色,并且它仍然很快时,我很高兴。我曾以为只有使用 OpenGL 才能达到这种性能水平。

我遇到的另一个我以前见过的情况是,线条没有完全渲染。可以看到一个间隙。当线条的端点远远超出屏幕之外,并且线条的角度非常小的时候,就会发生这种情况。我最终测试了这种情况并进行了纠正。如果有人知道为什么会发生这种情况以及如何防止它,请告诉我。

您会在代码中找到一些其他技巧来提高性能。例如,会进行测试以确定线条是否完全在屏幕外。如果是,则不将其绘制到缓冲区。进行此测试所花费的时间远远少于绘制到缓冲区。因此,您缩放得越靠近大型数据集,视图操作的速度就越快。

此外,System.Drawing.Drawing2D.GraphicsPath 类似乎非常适合此类项目,但它在大型数据集上表现缓慢。如上面的片段所示,接受点数组的 DrawLines 方法是我的选择。

在这个演示中,您找不到太多错误处理。我将其删除以保持简单。我还删除了整个机器配置窗体。

历史

这是我在 Code Project 上的第二篇文章,我希望它对他人有所价值。
当然,我欢迎建议和批评。

  • 2007年1月31日:初始发布
  • 2008 年 11 月 28 日:源代码已更新,包含 C#。
© . All rights reserved.