Merge pull request #3600 from sliptonic/feature/customimprove

[PATH] make -custom- op compliant. Improve gcode_pre importer
This commit is contained in:
sliptonic
2020-06-19 11:11:21 -05:00
committed by GitHub
11 changed files with 332 additions and 116 deletions

View File

@@ -34,6 +34,7 @@ SET(PathScripts_SRCS
PathScripts/PathComment.py
PathScripts/PathCopy.py
PathScripts/PathCustom.py
PathScripts/PathCustomGui.py
PathScripts/PathDeburr.py
PathScripts/PathDeburrGui.py
PathScripts/PathDressup.py

View File

@@ -98,6 +98,7 @@
<file>panels/PageBaseLocationEdit.ui</file>
<file>panels/PageDepthsEdit.ui</file>
<file>panels/PageHeightsEdit.ui</file>
<file>panels/PageOpCustomEdit.ui</file>
<file>panels/PageOpDeburrEdit.ui</file>
<file>panels/PageOpDrillingEdit.ui</file>
<file>panels/PageOpEngraveEdit.ui</file>

View File

@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>424</width>
<height>376</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QFrame" name="frame_2">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QGridLayout" name="gridLayout">
<property name="topMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item row="0" column="1">
<widget class="QComboBox" name="toolController">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The tool and its settings to be used for this operation.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>ToolController</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Coolant Mode</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="coolantController"/>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>G Gode</string>
</property>
</widget>
</item>
<item>
<widget class="QTextEdit" name="txtGCode">
<property name="lineWrapMode">
<enum>QTextEdit::NoWrap</enum>
</property>
<property name="acceptRichText">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -22,84 +22,58 @@
# ***************************************************************************
import FreeCAD
import FreeCADGui
import Path
import PathScripts.PathOp as PathOp
import PathScripts.PathLog as PathLog
from PySide import QtCore
from copy import copy
__title__ = "Path Custom Operation"
__author__ = "sliptonic (Brad Collette)"
__url__ = "http://www.freecadweb.org"
__doc__ = "Path Custom object and FreeCAD command"
__doc__ = """Path Custom object and FreeCAD command"""
if False:
PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
PathLog.trackModule(PathLog.thisModule())
else:
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
movecommands = ['G0', 'G00', 'G1', 'G01', 'G2', 'G02', 'G3', 'G03']
# Qt translation handling
def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
class ObjectCustom:
def __init__(self, obj):
class ObjectCustom(PathOp.ObjectOp):
def opFeatures(self, obj):
return PathOp.FeatureTool | PathOp.FeatureCoolant
def initOperation(self, obj):
obj.addProperty("App::PropertyStringList", "Gcode", "Path",
QtCore.QT_TRANSLATE_NOOP("PathCustom", "The gcode to be inserted"))
obj.addProperty("App::PropertyLink", "ToolController", "Path",
QtCore.QT_TRANSLATE_NOOP("PathCustom", "The tool controller that will be used to calculate the path"))
obj.addProperty("App::PropertyPlacement", "Offset", "Path",
"Placement Offset")
obj.Proxy = self
def __getstate__(self):
return None
def __setstate__(self, state):
return None
def execute(self, obj):
newpath = Path.Path()
def opExecute(self, obj):
self.commandlist.append(Path.Command("(Begin Custom)"))
if obj.Gcode:
for l in obj.Gcode:
newcommand = Path.Command(str(l))
if newcommand.Name in movecommands:
if 'X' in newcommand.Parameters:
newcommand.x += obj.Offset.Base.x
if 'Y' in newcommand.Parameters:
newcommand.y += obj.Offset.Base.y
if 'Z' in newcommand.Parameters:
newcommand.z += obj.Offset.Base.z
self.commandlist.append(newcommand)
newpath.insertCommand(newcommand)
obj.Path=newpath
self.commandlist.append(Path.Command("(End Custom)"))
class CommandPathCustom:
def GetResources(self):
return {'Pixmap': 'Path-Custom',
'MenuText': QtCore.QT_TRANSLATE_NOOP("Path_Custom", "Custom"),
'ToolTip': QtCore.QT_TRANSLATE_NOOP("Path_Custom", "Creates a path object based on custom G-code")}
def IsActive(self):
if FreeCAD.ActiveDocument is not None:
for o in FreeCAD.ActiveDocument.Objects:
if o.Name[:3] == "Job":
return True
return False
def Activated(self):
FreeCAD.ActiveDocument.openTransaction("Create Custom Path")
FreeCADGui.addModule("PathScripts.PathCustom")
FreeCADGui.addModule("PathScripts.PathUtils")
FreeCADGui.doCommand('obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", "Custom")')
FreeCADGui.doCommand('PathScripts.PathCustom.ObjectCustom(obj)')
FreeCADGui.doCommand('obj.ViewObject.Proxy = 0')
FreeCADGui.doCommand('PathScripts.PathUtils.addToJob(obj)')
FreeCADGui.doCommand('obj.ToolController = PathScripts.PathUtils.findToolController(obj)')
FreeCAD.ActiveDocument.commitTransaction()
FreeCAD.ActiveDocument.recompute()
def SetupProperties():
setup = []
return setup
if FreeCAD.GuiUp:
# register the FreeCAD command
FreeCADGui.addCommand('Path_Custom', CommandPathCustom())
def Create(name, obj=None):
'''Create(name) ... Creates and returns a Custom operation.'''
if obj is None:
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
proxy = ObjectCustom(obj, name)
return obj

View File

@@ -0,0 +1,86 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * *
# * Copyright (c) 2017 sliptonic <shopinthewoods@gmail.com> *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
import FreeCAD
import FreeCADGui
import PathScripts.PathCustom as PathCustom
import PathScripts.PathOpGui as PathOpGui
from PySide import QtCore
__title__ = "Path Custom Operation UI"
__author__ = "sliptonic (Brad Collette)"
__url__ = "http://www.freecadweb.org"
__doc__ = "Custom operation page controller and command implementation."
# Qt translation handling
def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
# class TaskPanelBaseGeometryPage(PathOpGui.TaskPanelBaseGeometryPage):
# '''Page controller for the base geometry.'''
# def getForm(self):
# return None
class TaskPanelOpPage(PathOpGui.TaskPanelPage):
'''Page controller class for the Custom operation.'''
def getForm(self):
'''getForm() ... returns UI'''
return FreeCADGui.PySideUic.loadUi(":/panels/PageOpCustomEdit.ui")
def getFields(self, obj):
'''getFields(obj) ... transfers values from UI to obj's properties'''
self.updateToolController(obj, self.form.toolController)
self.updateCoolant(obj, self.form.coolantController)
def setFields(self, obj):
'''setFields(obj) ... transfers obj's property values to UI'''
self.setupToolController(obj, self.form.toolController)
self.form.txtGCode.setText("\n".join(obj.Gcode))
self.setupCoolant(obj, self.form.coolantController)
def getSignalsForUpdate(self, obj):
'''getSignalsForUpdate(obj) ... return list of signals for updating obj'''
signals = []
signals.append(self.form.toolController.currentIndexChanged)
signals.append(self.form.coolantController.currentIndexChanged)
self.form.txtGCode.textChanged.connect(self.setGCode)
return signals
def setGCode(self):
self.obj.Gcode = self.form.txtGCode.toPlainText().splitlines()
Command = PathOpGui.SetupOperation('Custom', PathCustom.Create, TaskPanelOpPage,
'Path-Custom',
QtCore.QT_TRANSLATE_NOOP("Custom", "Custom"),
QtCore.QT_TRANSLATE_NOOP("Custom", "Create custom gcode snippet"),
PathCustom.SetupProperties)
FreeCAD.Console.PrintLog("Loading PathCustomGui... done\n")

View File

@@ -43,7 +43,8 @@ def Startup():
from PathScripts import PathAdaptiveGui
from PathScripts import PathArray
from PathScripts import PathComment
from PathScripts import PathCustom
# from PathScripts import PathCustom
from PathScripts import PathCustomGui
from PathScripts import PathDeburrGui
from PathScripts import PathDressupAxisMap
from PathScripts import PathDressupDogbone

View File

@@ -65,7 +65,7 @@ FeatureBasePanels = 0x0800 # Base
FeatureLocations = 0x1000 # Locations
FeatureCoolant = 0x2000 # Coolant
FeatureBaseGeometry = FeatureBaseVertexes | FeatureBaseFaces | FeatureBaseEdges | FeatureBasePanels | FeatureCoolant
FeatureBaseGeometry = FeatureBaseVertexes | FeatureBaseFaces | FeatureBaseEdges | FeatureBasePanels
class ObjectOp(object):
@@ -245,7 +245,7 @@ class ObjectOp(object):
def opFeatures(self, obj):
'''opFeatures(obj) ... returns the OR'ed list of features used and supported by the operation.
The default implementation returns "FeatureTool | FeatureDeptsh | FeatureHeights | FeatureStartPoint"
The default implementation returns "FeatureTool | FeatureDepths | FeatureHeights | FeatureStartPoint"
Should be overwritten by subclasses.'''
# pylint: disable=unused-argument
return FeatureTool | FeatureDepths | FeatureHeights | FeatureStartPoint | FeatureBaseGeometry | FeatureFinishDepth | FeatureCoolant

View File

@@ -91,6 +91,11 @@ class ViewProvider(object):
PathLog.track()
return hasattr(self, 'deleteOnReject') and self.deleteOnReject
def setDeleteObjectsOnReject(self, state=False):
PathLog.track()
self.deleteOnReject = state
return self.deleteOnReject
def setEdit(self, vobj=None, mode=0):
'''setEdit(vobj, mode=0) ... initiate editing of receivers model.'''
PathLog.track()
@@ -198,7 +203,7 @@ class TaskPanelPage(object):
Do not overwrite, implement initPage(obj) instead.'''
self.obj = obj
self.job = PathUtils.findParentJob(obj)
self.form = self.getForm() # pylint: disable=assignment-from-no-return
self.form = self.getForm() # pylint: disable=assignment-from-no-return
self.signalDirtyChanged = None
self.setClean()
self.setTitle('-')
@@ -285,32 +290,32 @@ class TaskPanelPage(object):
Note that this function is invoked after all page controllers have been created.
Should be overwritten by subclasses.'''
# pylint: disable=unused-argument
pass # pylint: disable=unnecessary-pass
pass # pylint: disable=unnecessary-pass
def cleanupPage(self, obj):
'''cleanupPage(obj) ... overwrite to perform any cleanup tasks before page is destroyed.
Can safely be overwritten by subclasses.'''
pass # pylint: disable=unnecessary-pass
pass # pylint: disable=unnecessary-pass
def modifyStandardButtons(self, buttonBox):
'''modifyStandardButtons(buttonBox) ... overwrite if the task panel standard buttons need to be modified.
Can safely be overwritten by subclasses.'''
pass # pylint: disable=unnecessary-pass
pass # pylint: disable=unnecessary-pass
def getForm(self):
'''getForm() ... return UI form for this page.
Must be overwritten by subclasses.'''
pass # pylint: disable=unnecessary-pass
pass # pylint: disable=unnecessary-pass
def getFields(self, obj):
'''getFields(obj) ... overwrite to transfer values from UI to obj's properties.
Can safely be overwritten by subclasses.'''
pass # pylint: disable=unnecessary-pass
pass # pylint: disable=unnecessary-pass
def setFields(self, obj):
'''setFields(obj) ... overwrite to transfer obj's property values to UI.
Can safely be overwritten by subclasses.'''
pass # pylint: disable=unnecessary-pass
pass # pylint: disable=unnecessary-pass
def getSignalsForUpdate(self, obj):
'''getSignalsForUpdate(obj) ... return signals which, when triggered, cause the receiver to update the model.
@@ -326,7 +331,7 @@ class TaskPanelPage(object):
manually.
Can safely be overwritten by subclasses.'''
# pylint: disable=unused-argument
pass # pylint: disable=unnecessary-pass
pass # pylint: disable=unnecessary-pass
def updateData(self, obj, prop):
'''updateData(obj, prop) ... overwrite if the receiver needs to react to property changes that might not have been caused by the receiver itself.
@@ -339,13 +344,13 @@ class TaskPanelPage(object):
In such a scenario the first property assignment will cause all changes in the UI of the other fields to be overwritten by setFields(obj).
You have been warned.'''
# pylint: disable=unused-argument
pass # pylint: disable=unnecessary-pass
pass # pylint: disable=unnecessary-pass
def updateSelection(self, obj, sel):
'''updateSelection(obj, sel) ... overwrite to customize UI depending on current selection.
Can safely be overwritten by subclasses.'''
# pylint: disable=unused-argument
pass # pylint: disable=unnecessary-pass
pass # pylint: disable=unnecessary-pass
# helpers
def selectInComboBox(self, name, combo):
@@ -432,7 +437,6 @@ class TaskPanelBaseGeometryPage(TaskPanelPage):
if len(availableOps) > 0:
# Populate the operations list
addInputs = True
panel.geometryImportList.blockSignals(True)
panel.geometryImportList.clear()
availableOps.sort()
@@ -738,7 +742,7 @@ class TaskPanelHeightsPage(TaskPanelPage):
self.safeHeight.updateProperty()
self.clearanceHeight.updateProperty()
def setFields(self, obj):
def setFields(self, obj):
self.safeHeight.updateSpinBox()
self.clearanceHeight.updateSpinBox()
@@ -933,7 +937,7 @@ class TaskPanel(object):
else:
self.featurePages.append(TaskPanelBaseLocationPage(obj, features))
if PathOp.FeatureDepths & features or PathOp.FeatureStepDown:
if PathOp.FeatureDepths & features or PathOp.FeatureStepDown & features:
if hasattr(opPage, 'taskPanelDepthsPage'):
self.featurePages.append(opPage.taskPanelDepthsPage(obj, features))
else:
@@ -1186,9 +1190,9 @@ class CommandPathOp:
self.res = resources
def GetResources(self):
ress = {'Pixmap': self.res.pixmap,
ress = {'Pixmap': self.res.pixmap,
'MenuText': self.res.menuText,
'ToolTip': self.res.toolTip}
'ToolTip': self.res.toolTip}
if self.res.accelKey:
ress['Accel'] = self.res.accelKey
return ress
@@ -1236,7 +1240,7 @@ def SetupOperation(name,
command = CommandPathOp(res)
FreeCADGui.addCommand("Path_%s" % name.replace(' ', '_'), command)
if not setupProperties is None:
if setupProperties is not None:
PathSetupSheet.RegisterOperation(name, objFactory, setupProperties)
return command

View File

@@ -290,6 +290,10 @@ def probeselect():
FreeCADGui.Selection.addSelectionGate(PROBEGate())
FreeCAD.Console.PrintWarning("Probe Select Mode\n")
def customselect():
FreeCAD.Console.PrintWarning("Custom Select Mode\n")
def select(op):
opsel = {}
@@ -309,6 +313,7 @@ def select(op):
opsel['Waterline'] = surfaceselect
opsel['Adaptive'] = adaptiveselect
opsel['Probe'] = probeselect
opsel['Custom'] = customselect
return opsel[op]

View File

@@ -25,7 +25,8 @@
'''
This is an example preprocessor file for the Path workbench. Its aim is to
open a gcode file, parse its contents, and create the appropriate objects
in FreeCAD.
in FreeCAD. This preprocessor will not add imported gcode to an existing
job. For a more useful preprocessor, look at the gcode_pre.py file
Read the Path Workbench documentation to know how to create Path objects
from GCode.
@@ -76,8 +77,8 @@ def parse(inputstring):
PathLog.track(inputstring)
# split the input by line
lines = inputstring.split("\n")
output = [] #""
lastcommand = None
output = []
lastcommand = None
for lin in lines:
# remove any leftover trailing and preceding spaces
@@ -98,8 +99,7 @@ def parse(inputstring):
continue
if lin[0].upper() in ["G", "M"]:
# found a G or M command: we store it
#output += lin + "\n"
output.append(Path.Command(str(lin))) # + "\n"
output.append(Path.Command(str(lin)))
last = lin[0].upper()
for c in lin[1:]:
if not c.isdigit():
@@ -109,7 +109,7 @@ def parse(inputstring):
lastcommand = last
elif lastcommand:
# no G or M command: we repeat the last one
output.append(Path.Command(str(lastcommand + " " + lin))) # + "\n"
output.append(Path.Command(str(lastcommand + " " + lin)))
print("done preprocessing.")
return output

View File

@@ -27,16 +27,29 @@ This is an example preprocessor file for the Path workbench. Its aim is to
open a gcode file, parse its contents, and create the appropriate objects
in FreeCAD.
This preprocessor will split gcode on tool changes and create one or more
PathCustom objects in the job. Tool Change commands themselves are not
preserved. It is up to the user to create and assign appropriate tool
controllers.
Only gcodes that are supported by Path are imported. Thus things like G43
are suppressed.
Importing gcode is inherently dangerous because context cannot be safely
assumed. The user should carefully examine the resulting gcode!
Read the Path Workbench documentation to know how to create Path objects
from GCode.
'''
import os
import Path
import FreeCAD
import PathScripts.PathUtils
import PathScripts.PathUtils as PathUtils
import PathScripts.PathLog as PathLog
import re
import PathScripts.PathCustom as PathCustom
import PathScripts.PathCustomGui as PathCustomGui
import PathScripts.PathOpGui as PathOpGui
# LEVEL = PathLog.Level.DEBUG
LEVEL = PathLog.Level.INFO
@@ -59,71 +72,109 @@ def open(filename):
insert(filename, doc.Name)
def matchToolController(op, toolnumber):
"""Try to match a tool controller in the job by number"""
toolcontrollers = PathUtils.getToolControllers(op)
for tc in toolcontrollers:
if tc.ToolNumber == toolnumber:
return tc
return toolcontrollers[0]
def insert(filename, docname):
"called when freecad imports a file"
PathLog.track(filename)
gfile = pythonopen(filename)
gcode = gfile.read()
gfile.close()
# split on tool changes
paths = re.split('(?=[mM]+\s?0?6)', gcode)
# if there are any tool changes combine the preamble with the default tool
if len(paths) > 1:
paths = ["\n".join(paths[0:2])] + paths[2:]
# Regular expression to match tool changes in the format 'M6 Tn'
p = re.compile('[mM]+?\s?0?6\s?T\d*\s')
# split the gcode on tool changes
paths = re.split('([mM]+?\s?0?6\s?T\d*\s)', gcode)
# iterate the gcode sections and add customs for each
toolnumber = 0
for path in paths:
# if the section is a tool change, extract the tool number
m = p.match(path)
if m:
toolnumber = int(m.group().split('T')[-1])
continue
# Parse the gcode and throw away any empty lists
gcode = parse(path)
doc = FreeCAD.getDocument(docname)
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", "Custom")
PathScripts.PathCustom.ObjectCustom(obj)
obj.ViewObject.Proxy = 0
if len(gcode) == 0:
continue
# Create a custom and viewobject
obj = PathCustom.Create("Custom")
res = PathOpGui.CommandResources('Custom', PathCustom.Create, PathCustomGui.TaskPanelOpPage, 'Path-Custom', 'Path-Custom', '', '')
obj.ViewObject.Proxy = PathOpGui.ViewProvider(obj.ViewObject, res)
obj.ViewObject.Proxy.setDeleteObjectsOnReject(False)
# Set the gcode and try to match a tool controller
obj.Gcode = gcode
PathScripts.PathUtils.addToJob(obj)
obj.ToolController = PathScripts.PathUtils.findToolController(obj)
obj.ToolController = matchToolController(obj, toolnumber)
FreeCAD.ActiveDocument.recompute()
def parse(inputstring):
"parse(inputstring): returns a parsed output string"
supported = ['G0', 'G00',
'G1', 'G01',
'G2', 'G02',
'G3', 'G03',
'G81', 'G82', 'G83',
'G90', 'G91']
axis = ["X", "Y", "Z", "A", "B", "C", "U", "V", "W"]
print("preprocessing...")
PathLog.track(inputstring)
# split the input by line
lines = inputstring.split("\n")
output = [] #""
lastcommand = None
lines = inputstring.splitlines()
output = []
lastcommand = None
for lin in lines:
# remove any leftover trailing and preceding spaces
lin = lin.strip()
# discard empty lines
if not lin:
# discard empty lines
continue
# remove line numbers
if lin[0].upper() in ["N"]:
# remove line numbers
lin = lin.split(" ", 1)
if len(lin) >= 1:
lin = lin[1].strip()
else:
continue
if lin[0] in ["(", "%", "#", ";"]:
# discard comment and other non strictly gcode lines
# Anything else not a G/M code or an axis move is ignored.
if lin[0] not in ["G", "M", "X", "Y", "Z", "A", "B", "C", "U", "V", "W"]:
continue
if lin[0].upper() in ["G", "M"]:
# found a G or M command: we store it
#output += lin + "\n"
output.append(lin) # + "\n"
last = lin[0].upper()
for c in lin[1:]:
if not c.isdigit():
break
else:
last += c
lastcommand = last
elif lastcommand:
# no G or M command: we repeat the last one
output.append(lastcommand + " " + lin) # + "\n"
# if the remaining line is supported, store it
currcommand = lin.split()[0]
if currcommand in supported:
output.append(lin)
lastcommand = currcommand
# modal commands have no G or M but have axis moves. append those too.
elif currcommand[0] in axis and lastcommand:
output.append(lastcommand + " " + lin)
print("done preprocessing.")
return output
print(__name__ + " gcode preprocessor loaded.")