diff --git a/src/Gui/ViewProviderGeometryObject.cpp b/src/Gui/ViewProviderGeometryObject.cpp index f943ecc565..7b3977710c 100644 --- a/src/Gui/ViewProviderGeometryObject.cpp +++ b/src/Gui/ViewProviderGeometryObject.cpp @@ -53,7 +53,7 @@ using namespace Gui; PROPERTY_SOURCE(Gui::ViewProviderGeometryObject, Gui::ViewProviderDragger) -const App::PropertyIntegerConstraint::Constraints intPercent = {0,100,1}; +const App::PropertyIntegerConstraint::Constraints intPercent = {0, 100, 1}; ViewProviderGeometryObject::ViewProviderGeometryObject() : pcBoundSwitch(nullptr) @@ -61,7 +61,7 @@ ViewProviderGeometryObject::ViewProviderGeometryObject() { ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/View"); bool randomColor = hGrp->GetBool("RandomColor", false); - float r,g,b; + float r, g, b; if (randomColor){ float fMax = (float)RAND_MAX; @@ -94,10 +94,9 @@ ViewProviderGeometryObject::ViewProviderGeometryObject() Selectable.setValue(enableSel); pcShapeMaterial = new SoMaterial; + pcShapeMaterial->diffuseColor.setValue(r, g, b); + pcShapeMaterial->transparency = float(initialTransparency); pcShapeMaterial->ref(); - //ShapeMaterial.touch(); materials are rarely used, so better to initialize with default shape color - ShapeColor.touch(); - Transparency.touch(); pcBoundingBox = new Gui::SoFCBoundingBox; pcBoundingBox->ref(); @@ -125,16 +124,16 @@ void ViewProviderGeometryObject::onChanged(const App::Property* prop) setSelectable(Sel); } else if (prop == &ShapeColor) { - const App::Color& c = ShapeColor.getValue(); - pcShapeMaterial->diffuseColor.setValue(c.r,c.g,c.b); + const App::Color &c = ShapeColor.getValue(); + pcShapeMaterial->diffuseColor.setValue(c.r, c.g, c.b); if (c != ShapeMaterial.getValue().diffuseColor) - ShapeMaterial.setDiffuseColor(c); + ShapeMaterial.setDiffuseColor(c); } else if (prop == &Transparency) { - const App::Material& Mat = ShapeMaterial.getValue(); - long value = (long)(100*Mat.transparency); + const App::Material &Mat = ShapeMaterial.getValue(); + long value = (long)(100 * Mat.transparency); if (value != Transparency.getValue()) { - float trans = Transparency.getValue()/100.0f; + float trans = Transparency.getValue() / 100.0f; pcShapeMaterial->transparency = trans; ShapeMaterial.setTransparency(trans); } @@ -142,17 +141,17 @@ void ViewProviderGeometryObject::onChanged(const App::Property* prop) else if (prop == &ShapeMaterial) { if (getObject() && getObject()->testStatus(App::ObjectStatus::TouchOnColorChange)) getObject()->touch(true); - const App::Material& Mat = ShapeMaterial.getValue(); - long value = (long)(100*Mat.transparency); + const App::Material &Mat = ShapeMaterial.getValue(); + long value = (long)(100 * Mat.transparency); if (value != Transparency.getValue()) - Transparency.setValue(value); - const App::Color& color = Mat.diffuseColor; + Transparency.setValue(value); + const App::Color &color = Mat.diffuseColor; if (color != ShapeColor.getValue()) - ShapeColor.setValue(Mat.diffuseColor); - pcShapeMaterial->ambientColor.setValue(Mat.ambientColor.r,Mat.ambientColor.g,Mat.ambientColor.b); - pcShapeMaterial->diffuseColor.setValue(Mat.diffuseColor.r,Mat.diffuseColor.g,Mat.diffuseColor.b); - pcShapeMaterial->specularColor.setValue(Mat.specularColor.r,Mat.specularColor.g,Mat.specularColor.b); - pcShapeMaterial->emissiveColor.setValue(Mat.emissiveColor.r,Mat.emissiveColor.g,Mat.emissiveColor.b); + ShapeColor.setValue(Mat.diffuseColor); + pcShapeMaterial->ambientColor.setValue(Mat.ambientColor.r, Mat.ambientColor.g, Mat.ambientColor.b); + pcShapeMaterial->diffuseColor.setValue(Mat.diffuseColor.r, Mat.diffuseColor.g, Mat.diffuseColor.b); + pcShapeMaterial->specularColor.setValue(Mat.specularColor.r, Mat.specularColor.g, Mat.specularColor.b); + pcShapeMaterial->emissiveColor.setValue(Mat.emissiveColor.r, Mat.emissiveColor.g, Mat.emissiveColor.b); pcShapeMaterial->shininess.setValue(Mat.shininess); pcShapeMaterial->transparency.setValue(Mat.transparency); } diff --git a/src/Mod/Draft/DraftGui.py b/src/Mod/Draft/DraftGui.py index 7abc85b544..4fc91be56c 100644 --- a/src/Mod/Draft/DraftGui.py +++ b/src/Mod/Draft/DraftGui.py @@ -774,7 +774,7 @@ class DraftToolBar: "draft", "Continue") + " (" + inCommandShortcuts["Continue"][0] + ")") self.occOffset.setToolTip(translate( "draft", "If checked, an OCC-style offset will be performed" - + "instead of the classic offset")) + + " instead of the classic offset")) self.occOffset.setText(translate("draft", "&OCC-style offset")) # OBSOLETE diff --git a/src/Mod/Fem/App/FemConstraintBearing.cpp b/src/Mod/Fem/App/FemConstraintBearing.cpp index 57dcf89b41..2ca239ff1b 100644 --- a/src/Mod/Fem/App/FemConstraintBearing.cpp +++ b/src/Mod/Fem/App/FemConstraintBearing.cpp @@ -65,7 +65,7 @@ App::DocumentObjectExecReturn *ConstraintBearing::execute(void) void ConstraintBearing::onChanged(const App::Property* prop) { //Base::Console().Error("ConstraintBearing: onChanged %s\n", prop->getName()); - // Note: If we call this at the end, then the symbol ist not oriented correctly initially + // Note: If we call this at the end, then the symbol is not oriented correctly initially // because the NormalDirection has not been calculated yet Constraint::onChanged(prop); diff --git a/src/Mod/Fem/coding_conventions.md b/src/Mod/Fem/coding_conventions.md index 018e0def83..724fa0017d 100644 --- a/src/Mod/Fem/coding_conventions.md +++ b/src/Mod/Fem/coding_conventions.md @@ -8,9 +8,10 @@ These coding rules apply to FEM module code only. Other modules or the base syst ```bash # Find typos - codespell -q 2 -S *.ts -L childs,dof,dum,methode,nd,normaly,uint,vertexes,freez src/Mod/Fem/ + codespell -q 2 -S *.ts -S *.dyn -S *.svg -L childs,dof,dum,freez,methode,nd,normaly,programm,som,uint,vertexes,inout src/Mod/Fem/ + # Interactively fix said typos - codespell -i 3 -w -S *.ts -L childs,dof,dum,methode,nd,normaly,uint,vertexes,freez src/Mod/Fem/ + codespell -i 3 -w -S *.ts -S *.dyn -S *.svg -L childs,dof,dum,freez,methode,nd,normaly,programm,som,uint,vertexes,inout src/Mod/Fem/ ``` **Notes:** diff --git a/src/Mod/Fem/femguiutils/selection_widgets.py b/src/Mod/Fem/femguiutils/selection_widgets.py index c7ae25c214..2194e94c99 100644 --- a/src/Mod/Fem/femguiutils/selection_widgets.py +++ b/src/Mod/Fem/femguiutils/selection_widgets.py @@ -267,7 +267,7 @@ class GeometryElementsSelection(QtGui.QWidget): ) def initUI(self): - # auch ArchPanel ist coded ohne ui-file + # ArchPanel is coded without ui-file too # title self.setWindowTitle( self.tr("Geometry reference selector for a") + " " + self.sel_elem_text diff --git a/src/Mod/Fem/feminout/convert2TetGen.py b/src/Mod/Fem/feminout/convert2TetGen.py index cd93d52740..9f1846af43 100644 --- a/src/Mod/Fem/feminout/convert2TetGen.py +++ b/src/Mod/Fem/feminout/convert2TetGen.py @@ -47,7 +47,7 @@ def exportMeshToTetGenPoly(meshToExport, filePath, beVerbose=1): """Export mesh to TetGen *.poly file format""" # ********** Part 1 - write node list to output file if beVerbose == 1: - Console.PrintMessage("\nExport of mesh to TetGen file ...") + Console.PrintMessage("\nExport of mesh to TetGen file ...") (allVertices, allFacets) = meshToExport.Topology f = open(filePath, "w") f.write("# This file was generated from FreeCAD geometry\n") diff --git a/src/Mod/Fem/feminout/importCcxFrdResults.py b/src/Mod/Fem/feminout/importCcxFrdResults.py index e5e93df382..f916f273e0 100644 --- a/src/Mod/Fem/feminout/importCcxFrdResults.py +++ b/src/Mod/Fem/feminout/importCcxFrdResults.py @@ -215,9 +215,9 @@ def importFrd( # see error message above for more information if not res_obj: if result_name_prefix: - results_name = ("{}_Results".format(result_name_prefix)) + results_name = "{}_Results".format(result_name_prefix) else: - results_name = ("Results".format(result_name_prefix)) + results_name = "Results" res_obj = ObjectsFem.makeResultMechanical(doc, results_name) res_obj.Mesh = result_mesh_object # TODO, node numbers in result obj could be set diff --git a/src/Mod/Fem/feminout/importZ88Mesh.py b/src/Mod/Fem/feminout/importZ88Mesh.py index 0d1576d8b7..eb184b3159 100644 --- a/src/Mod/Fem/feminout/importZ88Mesh.py +++ b/src/Mod/Fem/feminout/importZ88Mesh.py @@ -180,7 +180,7 @@ def read_z88_mesh( nodes_count = int(mesh_info[1]) elements_count = int(mesh_info[2]) kflag = int(mesh_info[4]) - # for non rotational elements ist --> kflag = 0 --> cartesian, kflag = 1 polar coordinates + # for non rotational elements is --> kflag = 0 --> cartesian, kflag = 1 polar coordinates if kflag: Console.PrintError( "KFLAG = 1, Rotational coordinates not supported at the moment\n" diff --git a/src/Mod/Fem/feminout/importZ88O2Results.py b/src/Mod/Fem/feminout/importZ88O2Results.py index caa522c27f..566eb3989e 100644 --- a/src/Mod/Fem/feminout/importZ88O2Results.py +++ b/src/Mod/Fem/feminout/importZ88O2Results.py @@ -68,8 +68,8 @@ def insert( # ********* module specific methods ********* def import_z88_disp( filename, - analysis = None, - result_name_prefix = None + analysis=None, + result_name_prefix=None ): """insert a FreeCAD FEM mechanical result object in the ActiveDocument pure usage: diff --git a/src/Mod/Fem/femmesh/gmshtools.py b/src/Mod/Fem/femmesh/gmshtools.py index 44aafda0de..fba0ba36db 100644 --- a/src/Mod/Fem/femmesh/gmshtools.py +++ b/src/Mod/Fem/femmesh/gmshtools.py @@ -735,7 +735,7 @@ class GmshTools(): geo.write("\n") cpu_count = os.cpu_count() - if cpu_count != None and cpu_count > 1: + if cpu_count is not None and cpu_count > 1: geo.write("// enable multi-core processing\n") geo.write(f"General.NumThreads = {cpu_count};\n") geo.write("\n") diff --git a/src/Mod/Fem/femmesh/meshtools.py b/src/Mod/Fem/femmesh/meshtools.py index dd8671b0ea..d09b187fcd 100644 --- a/src/Mod/Fem/femmesh/meshtools.py +++ b/src/Mod/Fem/femmesh/meshtools.py @@ -27,10 +27,12 @@ __url__ = "https://www.freecadweb.org" ## \addtogroup FEM # @{ +import numpy as np + import FreeCAD from femtools import geomtools -import numpy as np + # ************************************************************************************************ def get_femnodes_by_femobj_with_references( @@ -485,7 +487,10 @@ def get_femelement_sets( # fem_objects = FreeCAD FEM document objects # get femelements for reference shapes of each obj.References count_femelements = 0 - referenced_femelements = np.zeros((max(femelement_table.keys())+1,),dtype=np.int) + referenced_femelements = np.zeros( + (max(femelement_table.keys()) + 1,), + dtype=np.int + ) has_remaining_femelements = None for fem_object_i, fem_object in enumerate(fem_objects): obj = fem_object["Object"] @@ -515,7 +520,9 @@ def get_femelement_sets( femelement_table_array = np.zeros_like(referenced_femelements) femelement_table_array[list(femelement_table.keys())] = 1 remaining_femelements_array = femelement_table_array > referenced_femelements - remaining_femelements = [ i.item() for i in np.nditer(remaining_femelements_array.nonzero()) ] + remaining_femelements = [ + i.item() for i in np.nditer(remaining_femelements_array.nonzero()) + ] count_femelements += len(remaining_femelements) for fem_object in fem_objects: obj = fem_object["Object"] @@ -933,7 +940,7 @@ def get_force_obj_edge_nodeload_table( # try debugging of the last bad refedge FreeCAD.Console.PrintMessage("DEBUGGING\n") - FreeCAD.Console.PrintMessage("\n".format(bad_refedge)) + FreeCAD.Console.PrintMessage("{}\n".format(bad_refedge)) FreeCAD.Console.PrintMessage("bad_refedge_nodes\n") bad_refedge_nodes = femmesh.getNodesByEdge(bad_refedge) @@ -949,7 +956,7 @@ def get_force_obj_edge_nodeload_table( FreeCAD.Console.PrintMessage("{}\n".format(len(bad_edge_table))) bad_edge_table_nodes = [] for elem in bad_edge_table: - FreeCAD.Console.PrintMessage(elem, " --> \n".format(bad_edge_table[elem])) + FreeCAD.Console.PrintMessage(elem, " --> {}\n".format(bad_edge_table[elem])) for node in bad_edge_table[elem]: if node not in bad_edge_table_nodes: bad_edge_table_nodes.append(node) @@ -1997,7 +2004,7 @@ def get_reference_group_elements( FreeCAD.Console.PrintError( "Error, two refshapes in References with different ShapeTypes.\n" ) - FreeCAD.Console.PrintLog("\n".format(ref_shape)) + FreeCAD.Console.PrintLog("{}\n".format(ref_shape)) found_element = geomtools.find_element_in_shape(aShape, ref_shape) if found_element is not None: elements.append(found_element) @@ -2162,8 +2169,8 @@ def sortlistoflistvalues( listoflists ): new_list = [] - for l in listoflists: - new_list.append(sorted(l)) + for li in listoflists: + new_list.append(sorted(li)) return new_list @@ -2213,7 +2220,7 @@ def is_zplane_2D_mesh( for n in femmesh.Nodes: z = femmesh.Nodes[n].z if ((0 - tol) < z < (0 + tol)) is not True: - return False + return False return True else: return False diff --git a/src/Mod/Fem/femobjects/constraint_electrostaticpotential.py b/src/Mod/Fem/femobjects/constraint_electrostaticpotential.py index dde97a7e39..640188ad7f 100644 --- a/src/Mod/Fem/femobjects/constraint_electrostaticpotential.py +++ b/src/Mod/Fem/femobjects/constraint_electrostaticpotential.py @@ -30,7 +30,6 @@ __url__ = "https://www.freecadweb.org" # \ingroup FEM # \brief constraint electrostatic potential object -from FreeCAD import Units from . import base_fempythonobject diff --git a/src/Mod/Fem/femsolver/calculix/write_femelement_geometry.py b/src/Mod/Fem/femsolver/calculix/write_femelement_geometry.py index c6b23c36f3..b8aebe1d9b 100644 --- a/src/Mod/Fem/femsolver/calculix/write_femelement_geometry.py +++ b/src/Mod/Fem/femsolver/calculix/write_femelement_geometry.py @@ -37,7 +37,7 @@ def write_femelement_geometry(f, ccxwriter): elsetdef = "ELSET={}, ".format(matgeoset["ccx_elset_name"]) material = "MATERIAL={}".format(matgeoset["mat_obj_name"]) - if "beamsection_obj"in matgeoset: # beam mesh + if "beamsection_obj" in matgeoset: # beam mesh beamsec_obj = matgeoset["beamsection_obj"] beam_axis_m = matgeoset["beam_axis_m"] # in CalxuliX called the 1direction diff --git a/src/Mod/Fem/femsolver/elmer/tasks.py b/src/Mod/Fem/femsolver/elmer/tasks.py index 6afb35848e..ab3a1f0c3d 100644 --- a/src/Mod/Fem/femsolver/elmer/tasks.py +++ b/src/Mod/Fem/femsolver/elmer/tasks.py @@ -121,10 +121,22 @@ class Solve(run.Solve): if os.path.isdir(solvpath): os.environ["ELMER_HOME"] = solvpath os.environ["LD_LIBRARY_PATH"] = "$LD_LIBRARY_PATH:{}/modules".format(solvpath) - self._process = subprocess.Popen( - [binary], cwd=self.directory, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + # hide the popups on Windows + if system() == "Windows": + self._process = subprocess.Popen( + [binary], + cwd=self.directory, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + startupinfo=femutils.startProgramInfo("hide") + ) + else: + self._process = subprocess.Popen( + [binary], + cwd=self.directory, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) self.signalAbort.add(self._process.terminate) output = self._observeSolver(self._process) self._process.communicate() diff --git a/src/Mod/Fem/femsolver/elmer/writer.py b/src/Mod/Fem/femsolver/elmer/writer.py index bd016cd1bd..429f3ea58d 100644 --- a/src/Mod/Fem/femsolver/elmer/writer.py +++ b/src/Mod/Fem/femsolver/elmer/writer.py @@ -33,6 +33,7 @@ import os import os.path import subprocess import tempfile +from platform import system from FreeCAD import Console from FreeCAD import Units @@ -95,7 +96,14 @@ class Writer(object): self._writeStartinfo() def _handleUnits(self): - # TODO constants and units + # Elmer solver writer no longer uses FreeCAD unit system + # to retrieve units for writing the sif file + # + # ATM Elmer writer uses SI units only + # + # see forum topic: https://forum.freecadweb.org/viewtopic.php?f=18&t=70150 + # + # TODO: adapt method and comment # should be only one system for all solver and not in each solver # https://forum.freecadweb.org/viewtopic.php?t=47895 # https://forum.freecadweb.org/viewtopic.php?t=48451 @@ -216,7 +224,15 @@ class Writer(object): unvPath, "-scale", "0.001", "0.001", "0.001", "-out", self.directory] - subprocess.call(args, stdout=subprocess.DEVNULL) + # hide the popups on Windows + if system() == "Windows": + subprocess.call( + args, + stdout=subprocess.DEVNULL, + startupinfo=femutils.startProgramInfo("hide") + ) + else: + subprocess.call(args, stdout=subprocess.DEVNULL) def _writeStartinfo(self): path = os.path.join(self.directory, _STARTINFO_NAME) @@ -282,11 +298,11 @@ class Writer(object): self._simulation("Coordinate System", "Cartesian 3D") self._simulation("Coordinate Mapping", (1, 2, 3)) # not necessary anymore since we use SI units - #if self.unit_schema == Units.Scheme.SI2: - #self._simulation("Coordinate Scaling", 0.001) - # Console.PrintMessage( - # "'Coordinate Scaling = Real 0.001' was inserted into the solver input file.\n" - # ) + # if self.unit_schema == Units.Scheme.SI2: + # self._simulation("Coordinate Scaling", 0.001) + # Console.PrintMessage( + # "'Coordinate Scaling = Real 0.001' was inserted into the solver input file.\n" + # ) self._simulation("Simulation Type", "Steady state") self._simulation("Steady State Max Iterations", 1) self._simulation("Output Intervals", 1) diff --git a/src/Mod/Fem/femsolver/z88/tasks.py b/src/Mod/Fem/femsolver/z88/tasks.py index 0d54810cfa..0a0b15c4cc 100644 --- a/src/Mod/Fem/femsolver/z88/tasks.py +++ b/src/Mod/Fem/femsolver/z88/tasks.py @@ -31,6 +31,7 @@ __url__ = "https://www.freecadweb.org" import os import os.path import subprocess +from platform import system import FreeCAD @@ -44,6 +45,7 @@ from femtools import membertools SOLVER_TYPES = ["sorcg", "siccg", "choly"] + class Check(run.Check): def run(self): @@ -103,7 +105,7 @@ class Solve(run.Solve): binary = settings.get_binary("Z88") if binary is None: self.fail() # a print has been made in settings module - + prefs = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Fem/Z88") solver = SOLVER_TYPES solver = prefs.GetInt("Solver", 0) @@ -117,23 +119,30 @@ class Solve(run.Solve): # TODO: search out for "Vector GS" and "Vector KOI" and print values # may be compare with the used ones self.pushStatus("Executing solver in test mode...\n") - self._process = subprocess.Popen( - [binary, "-t", "-" + solver], - cwd=self.directory, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - self.signalAbort.add(self._process.terminate) - self._process.communicate() - self.signalAbort.remove(self._process.terminate) + Solve.runZ88(self, "-t", binary, solver, "hide") # run solver real mode self.pushStatus("Executing solver in real mode...\n") - binary = settings.get_binary("Z88") - self._process = subprocess.Popen( - [binary, "-c", "-" + solver], - cwd=self.directory, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + # starting normal because the user must see the z88 window + Solve.runZ88(self, "-c", binary, solver, "normal") + + def runZ88(self, command, binary, solver, state): + # minimize or hide the popups on Windows + if system() == "Windows": + self._process = subprocess.Popen( + [binary, command, "-" + solver], + cwd=self.directory, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + startupinfo=femutils.startProgramInfo(state) + ) + else: + self._process = subprocess.Popen( + [binary, command, "-" + solver], + cwd=self.directory, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) self.signalAbort.add(self._process.terminate) self._process.communicate() self.signalAbort.remove(self._process.terminate) diff --git a/src/Mod/Fem/femtaskpanels/task_constraint_electrostaticpotential.py b/src/Mod/Fem/femtaskpanels/task_constraint_electrostaticpotential.py index 1206a559f2..8cac07b7a1 100644 --- a/src/Mod/Fem/femtaskpanels/task_constraint_electrostaticpotential.py +++ b/src/Mod/Fem/femtaskpanels/task_constraint_electrostaticpotential.py @@ -33,6 +33,7 @@ __url__ = "https://www.freecadweb.org" import FreeCAD import FreeCADGui from femguiutils import selection_widgets + from femtools import femutils from femtools import membertools @@ -111,7 +112,6 @@ class _TaskPanel(object): not self._paramWidget.capacitanceBodyBox.isChecked()) def _applyWidgetChanges(self): - unit = "V" self._obj.PotentialEnabled = \ not self._paramWidget.potentialBox.isChecked() if self._obj.PotentialEnabled: diff --git a/src/Mod/Fem/femtaskpanels/task_material_common.py b/src/Mod/Fem/femtaskpanels/task_material_common.py index a29673d52d..8568122972 100644 --- a/src/Mod/Fem/femtaskpanels/task_material_common.py +++ b/src/Mod/Fem/femtaskpanels/task_material_common.py @@ -206,9 +206,9 @@ class _TaskPanel: def accept(self): # print(self.material) if self.material == {}: # happens if material editor was canceled - FreeCAD.Console.PrintError("Empty material dictionary, nothing was changed.\n") - self.recompute_and_set_back_all() - return True + FreeCAD.Console.PrintError("Empty material dictionary, nothing was changed.\n") + self.recompute_and_set_back_all() + return True if self.selectionWidget.has_equal_references_shape_types(): self.do_not_set_thermal_zeros() from materialtools.cardutils import check_mat_units as checkunits @@ -488,7 +488,8 @@ class _TaskPanel: self.material["VolumetricThermalExpansionCoefficient"] = "0 1/K" else: if "ThermalExpansionCoefficient" in self.material: - self.material["VolumetricThermalExpansionCoefficient"] = self.material["ThermalExpansionCoefficient"] + the_index = "VolumetricThermalExpansionCoefficient" # line was to long + self.material[the_index] = self.material["ThermalExpansionCoefficient"] else: self.material["VolumetricThermalExpansionCoefficient"] = "0 1/K" # Thermal properties @@ -737,4 +738,4 @@ class _TaskPanel: self.parameterWidget.cb_materials.addItem(QtGui.QIcon(mat[2]), mat[0], mat[1]) # the whole card path is added to the combo box to make it unique # see def choose_material: - # for assignment of self.card_path the path form the parameterWidget ist used + # for assignment of self.card_path the path form the parameterWidget is used diff --git a/src/Mod/Fem/femtaskpanels/task_material_reinforced.py b/src/Mod/Fem/femtaskpanels/task_material_reinforced.py index 75fa46f684..03db15f395 100644 --- a/src/Mod/Fem/femtaskpanels/task_material_reinforced.py +++ b/src/Mod/Fem/femtaskpanels/task_material_reinforced.py @@ -432,4 +432,4 @@ class _TaskPanel: self.parameterWidget.cb_materials_r.addItem(QtGui.QIcon(mat[2]), mat[0], mat[1]) # the whole card path is added to the combo box to make it unique # see def choose_material: - # for assignment of self.card_path the path form the parameterWidget ist used + # for assignment of self.card_path the path form the parameterWidget is used diff --git a/src/Mod/Fem/femtaskpanels/task_result_mechanical.py b/src/Mod/Fem/femtaskpanels/task_result_mechanical.py index 0b867b1b5c..799ec748c2 100644 --- a/src/Mod/Fem/femtaskpanels/task_result_mechanical.py +++ b/src/Mod/Fem/femtaskpanels/task_result_mechanical.py @@ -46,7 +46,6 @@ from PySide.QtGui import QApplication import FreeCAD import FreeCADGui -import FemGui import femresult.resulttools as resulttools @@ -287,7 +286,12 @@ class _TaskPanel: def abs_displacement_selected(self, state): if len(self.result_obj.DisplacementLengths) > 0: - self.result_selected("Uabs", self.result_obj.DisplacementLengths, "mm", translate("FEM","Displacement Magnitude")) + self.result_selected( + "Uabs", + self.result_obj.DisplacementLengths, + "mm", + translate("FEM", "Displacement Magnitude") + ) else: self.result_widget.rb_none.setChecked(True) self.none_selected(True) @@ -297,7 +301,12 @@ class _TaskPanel: res_disp_u1 = self.get_scalar_disp_list( self.result_obj.DisplacementVectors, 0 ) - self.result_selected("U1", res_disp_u1, "mm", translate("FEM","Displacement X")) + self.result_selected( + "U1", + res_disp_u1, + "mm", + translate("FEM", "Displacement X") + ) else: self.result_widget.rb_none.setChecked(True) self.none_selected(True) @@ -307,7 +316,12 @@ class _TaskPanel: res_disp_u2 = self.get_scalar_disp_list( self.result_obj.DisplacementVectors, 1 ) - self.result_selected("U2", res_disp_u2, "mm", translate("FEM","Displacement Y")) + self.result_selected( + "U2", + res_disp_u2, + "mm", + translate("FEM", "Displacement Y") + ) else: self.result_widget.rb_none.setChecked(True) self.none_selected(True) @@ -317,63 +331,108 @@ class _TaskPanel: res_disp_u3 = self.get_scalar_disp_list( self.result_obj.DisplacementVectors, 2 ) - self.result_selected("U3", res_disp_u3, "mm", translate("FEM","Displacement Z")) + self.result_selected( + "U3", + res_disp_u3, + "mm", + translate("FEM", "Displacement Z") + ) else: self.result_widget.rb_none.setChecked(True) self.none_selected(True) def vm_stress_selected(self, state): if len(self.result_obj.vonMises) > 0: - self.result_selected("Sabs", self.result_obj.vonMises, "MPa", translate("FEM","von Mises Stress")) + self.result_selected( + "Sabs", + self.result_obj.vonMises, + "MPa", + translate("FEM", "von Mises Stress") + ) else: self.result_widget.rb_none.setChecked(True) self.none_selected(True) def max_shear_selected(self, state): if len(self.result_obj.MaxShear) > 0: - self.result_selected("MaxShear", self.result_obj.MaxShear, "MPa", translate("FEM","Max Shear Stress")) + self.result_selected( + "MaxShear", + self.result_obj.MaxShear, + "MPa", + translate("FEM", "Max Shear Stress") + ) else: self.result_widget.rb_none.setChecked(True) self.none_selected(True) def max_prin_selected(self, state): if len(self.result_obj.PrincipalMax) > 0: - self.result_selected("MaxPrin", self.result_obj.PrincipalMax, "MPa", translate("FEM","Max Principal Stress")) + self.result_selected( + "MaxPrin", + self.result_obj.PrincipalMax, + "MPa", + translate("FEM", "Max Principal Stress") + ) else: self.result_widget.rb_none.setChecked(True) self.none_selected(True) def temperature_selected(self, state): if len(self.result_obj.Temperature) > 0: - self.result_selected("Temp", self.result_obj.Temperature, "K", translate("FEM","Temperature")) + self.result_selected( + "Temp", + self.result_obj.Temperature, + "K", + translate("FEM", "Temperature") + ) else: self.result_widget.rb_none.setChecked(True) self.none_selected(True) def massflowrate_selected(self, state): if len(self.result_obj.MassFlowRate) > 0: - self.result_selected("MFlow", self.result_obj.MassFlowRate, "kg/s", translate("FEM","Mass Flow Rate")) + self.result_selected( + "MFlow", + self.result_obj.MassFlowRate, + "kg/s", + translate("FEM", "Mass Flow Rate") + ) else: self.result_widget.rb_none.setChecked(True) self.none_selected(True) def networkpressure_selected(self, state): if len(self.result_obj.NetworkPressure) > 0: - self.result_selected("NPress", self.result_obj.NetworkPressure, "MPa", translate("FEM","Network Pressure")) + self.result_selected( + "NPress", + self.result_obj.NetworkPressure, + "MPa", + translate("FEM", "Network Pressure") + ) else: self.result_widget.rb_none.setChecked(True) self.none_selected(True) def min_prin_selected(self, state): if len(self.result_obj.PrincipalMin) > 0: - self.result_selected("MinPrin", self.result_obj.PrincipalMin, "MPa", translate("FEM","Min Principal Stress")) + self.result_selected( + "MinPrin", + self.result_obj.PrincipalMin, + "MPa", + translate("FEM", "Min Principal Stress") + ) else: self.result_widget.rb_none.setChecked(True) self.none_selected(True) def peeq_selected(self, state): if len(self.result_obj.Peeq) > 0: - self.result_selected("Peeq", self.result_obj.Peeq, "", translate("FEM","Equivalent Plastic Strain")) + self.result_selected( + "Peeq", + self.result_obj.Peeq, + "", + translate("FEM", "Equivalent Plastic Strain") + ) else: self.result_widget.rb_none.setChecked(True) self.none_selected(True) @@ -392,8 +451,8 @@ class _TaskPanel: else: QtGui.QMessageBox.information( None, - self.result_obj.Label + " - " + translate("FEM","Information"), - translate("FEM","No histogram available.\nPlease select a result type first.") + self.result_obj.Label + " - " + translate("FEM", "Information"), + translate("FEM", "No histogram available.\nPlease select a result type first.") ) def user_defined_text(self, equation): @@ -503,12 +562,12 @@ class _TaskPanel: if len(plt.get_fignums()) > 0: plt.close() - plt.ioff() # disable interactive mode so we have full control when plot is shown + plt.ioff() # disable interactive mode so we have full control when plot is shown plt.figure(res_title) plt.hist(res_values, bins=50, alpha=0.5, facecolor="blue") plt.xlabel(res_unit) - plt.title(translate("FEM","Histogram of {}").format(res_title)) - plt.ylabel(translate("FEM","Nodes")) + plt.title(translate("FEM", "Histogram of {}").format(res_title)) + plt.ylabel(translate("FEM", "Nodes")) plt.grid(True) fig_manager = plt.get_current_fig_manager() # Lines below tells Qt that plot widget/dialog should be kept on top of its parent, @@ -628,25 +687,49 @@ class _TaskPanel: self.suitable_results = False self.disable_empty_result_buttons() if self.mesh_obj.FemMesh.NodeCount == 0: - error_message = ( - translate("FEM","FEM: there are no nodes in result mesh, there will be nothing to show.") + "\n" - ) - FreeCAD.Console.PrintError(error_message) - QtGui.QMessageBox.critical(None, translate("FEM","Empty result mesh"), error_message) + the_error_messagetext = ( + "FEM: there are no nodes in result mesh, " + "there will be nothing to show." + ) + error_message = ( + translate("FEM", the_error_messagetext) + "\n" + ) + FreeCAD.Console.PrintError(error_message) + QtGui.QMessageBox.critical( + None, + translate("FEM", "Empty result mesh"), + error_message + ) elif (self.mesh_obj.FemMesh.NodeCount == len(self.result_obj.NodeNumbers)): self.suitable_results = True hide_parts_constraints() else: if not self.mesh_obj.FemMesh.VolumeCount: + the_error_messagetext = ( + "FEM: Graphical bending stress output " + "for beam or shell FEM Meshes not yet supported." + ) error_message = ( - translate("FEM","FEM: Graphical bending stress output for beam or shell FEM Meshes not yet supported.") + "\n" + translate("FEM", the_error_messagetext) + "\n" ) FreeCAD.Console.PrintError(error_message) - QtGui.QMessageBox.critical(None, translate("FEM","No result object"), error_message) + QtGui.QMessageBox.critical( + None, + translate("FEM", "No result object"), + error_message + ) else: - error_message = translate("FEM","FEM: Result node numbers are not equal to FEM Mesh NodeCount.") + "\n" + the_error_messagetext = ( + "FEM: Result node numbers are " + "not equal to FEM Mesh NodeCount." + ) + error_message = translate("FEM", the_error_messagetext) + "\n" FreeCAD.Console.PrintError(error_message) - QtGui.QMessageBox.critical(None, translate("FEM","No result object"), error_message) + QtGui.QMessageBox.critical( + None, + translate("FEM", "No result object"), + error_message + ) def reset_mesh_color(self): self.mesh_obj.ViewObject.NodeColor = {} diff --git a/src/Mod/Fem/femtaskpanels/task_solver_ccxtools.py b/src/Mod/Fem/femtaskpanels/task_solver_ccxtools.py index ad386090b5..b38e65bd74 100644 --- a/src/Mod/Fem/femtaskpanels/task_solver_ccxtools.py +++ b/src/Mod/Fem/femtaskpanels/task_solver_ccxtools.py @@ -247,12 +247,14 @@ class _TaskPanel: self.form.pb_run_ccx.setText("Break CalculiX") def calculixStateChanged(self, newState): - if (newState == QtCore.QProcess.ProcessState.Starting): - self.femConsoleMessage("Starting CalculiX...") - if (newState == QtCore.QProcess.ProcessState.Running): - self.femConsoleMessage("CalculiX is running...") - if (newState == QtCore.QProcess.ProcessState.NotRunning): - self.femConsoleMessage("CalculiX stopped.") + if newState == QtCore.QProcess.ProcessState.Starting: + self.femConsoleMessage("Starting CalculiX...") + elif newState == QtCore.QProcess.ProcessState.Running: + self.femConsoleMessage("CalculiX is running...") + elif newState == QtCore.QProcess.ProcessState.NotRunning: + self.femConsoleMessage("CalculiX stopped.") + else: + self.femConsoleMessage("Problems.") def calculixFinished(self, exitCode): # print("calculixFinished(), exit code: {}".format(exitCode)) @@ -387,7 +389,7 @@ class _TaskPanel: env.insert("OMP_NUM_THREADS", str(num_cpu_pref)) else: cpu_count = os.cpu_count() - if cpu_count != None and cpu_count > 1: + if cpu_count is not None and cpu_count > 1: env.insert("OMP_NUM_THREADS", str(cpu_count)) self.Calculix.setProcessEnvironment(env) diff --git a/src/Mod/Fem/femtest/app/support_utils.py b/src/Mod/Fem/femtest/app/support_utils.py index e99981906c..b4dce7284a 100644 --- a/src/Mod/Fem/femtest/app/support_utils.py +++ b/src/Mod/Fem/femtest/app/support_utils.py @@ -88,14 +88,14 @@ def get_defmake_count( modfile = open(name_modfile, "r") lines_modefile = modfile.readlines() modfile.close() - lines_defmake = [l for l in lines_modefile if l.startswith("def make")] + lines_defmake = [li for li in lines_modefile if li.startswith("def make")] if not fem_vtk_post: # FEM VTK post processing is disabled # we are not able to create VTK post objects new_lines = [] - for l in lines_defmake: - if "PostVtk" not in l: - new_lines.append(l) + for li in lines_defmake: + if "PostVtk" not in li: + new_lines.append(li) lines_defmake = new_lines return len(lines_defmake) @@ -196,23 +196,23 @@ def compare_inp_files( # for python3 problem with 1DFlow input # TODO as soon as the 1DFlow result reading is fixed # this should be triggered in the 1DFlow unit test - lf1 = [l for l in f1 if not ( - l.startswith("** written ") or l.startswith("** file ") or l.startswith("17671.0,1") + lf1 = [li for li in f1 if not ( + li.startswith("** written ") or li.startswith("** file ") or li.startswith("17671.0,1") )] lf1 = force_unix_line_ends(lf1) file2 = open(file_name2, "r") f2 = file2.readlines() file2.close() # TODO see comment on file1 - lf2 = [l for l in f2 if not ( - l.startswith("** written ") or l.startswith("** file ") or l.startswith("17671.0,1") + lf2 = [li for li in f2 if not ( + li.startswith("** written ") or li.startswith("** file ") or li.startswith("17671.0,1") )] lf2 = force_unix_line_ends(lf2) import difflib diff = difflib.unified_diff(lf1, lf2, n=0) result = "" - for l in diff: - result += l + for li in diff: + result += li if result: result = ( "Comparing {} to {} failed!\n" @@ -234,22 +234,28 @@ def compare_files( # workaround to compare geos of elmer test and temporary file path # (not only names change, path changes with operating system) - lf1 = [l for l in f1 if not ( - l.startswith('Merge "') or l.startswith('Save "') or l.startswith("// ") or l.startswith("General.NumThreads") + lf1 = [li for li in f1 if not ( + li.startswith('Merge "') + or li.startswith('Save "') + or li.startswith("// ") + or li.startswith("General.NumThreads") )] lf1 = force_unix_line_ends(lf1) file2 = open(file_name2, "r") f2 = file2.readlines() file2.close() - lf2 = [l for l in f2 if not ( - l.startswith('Merge "') or l.startswith('Save "') or l.startswith("// ") or l.startswith("General.NumThreads") + lf2 = [li for li in f2 if not ( + li.startswith('Merge "') + or li.startswith('Save "') + or li.startswith("// ") + or li.startswith("General.NumThreads") )] lf2 = force_unix_line_ends(lf2) import difflib diff = difflib.unified_diff(lf1, lf2, n=0) result = "" - for l in diff: - result += l + for li in diff: + result += li if result: result = "Comparing {} to {} failed!\n".format(file_name1, file_name2) + result return result @@ -301,10 +307,10 @@ def compare_stats( # get stats to compare with, the expected ones sf = open(stat_file, "r") sf_content = [] - for l in sf.readlines(): + for li in sf.readlines(): for st in loc_stat_types: - if l.startswith(st): - sf_content.append(l) + if li.startswith(st): + sf_content.append(li) sf.close() sf_content = force_unix_line_ends(sf_content) if sf_content == []: diff --git a/src/Mod/Fem/femtools/femutils.py b/src/Mod/Fem/femtools/femutils.py index 435adf33d2..133c7eca1b 100644 --- a/src/Mod/Fem/femtools/femutils.py +++ b/src/Mod/Fem/femtools/femutils.py @@ -1,6 +1,7 @@ # *************************************************************************** # * Copyright (c) 2017 Markus Hovorka * # * Copyright (c) 2018 Bernd Hahnebach * +# * Copyright (c) 2022 Uwe Stöhr * # * * # * This file is part of the FreeCAD CAx development system. * # * * @@ -27,14 +28,14 @@ This module contains function for extracting relevant parts of geometry and a few unrelated function useful at various places in the Fem module. """ - __title__ = "FEM Utilities" -__author__ = "Markus Hovorka, Bernd Hahnebach" +__author__ = 'Markus Hovorka, Bernd Hahnebach, Uwe Stöhr' __url__ = "https://www.freecadweb.org" - import os import sys +import subprocess +from platform import system import FreeCAD if FreeCAD.GuiUp: @@ -387,3 +388,20 @@ def pydecode(bytestring): return bytestring else: return bytestring.decode("utf-8") + + +def startProgramInfo(code): + """ starts a program under Windows minimized, hidden or normal """ + if system() == "Windows": + info = subprocess.STARTUPINFO() + if code == "hide": + SW_HIDE = 0 + info.wShowWindow = SW_HIDE + elif code == "minimize": + SW_MINIMIZE = 6 + info.wShowWindow = SW_MINIMIZE + elif code == "normal": + SW_DEFAULT = 10 + info.wShowWindow = SW_DEFAULT + info.dwFlags = subprocess.STARTF_USESHOWWINDOW + return info diff --git a/src/Mod/Fem/femviewprovider/view_mesh_gmsh.py b/src/Mod/Fem/femviewprovider/view_mesh_gmsh.py index b9da6dd11b..81be7da090 100644 --- a/src/Mod/Fem/femviewprovider/view_mesh_gmsh.py +++ b/src/Mod/Fem/femviewprovider/view_mesh_gmsh.py @@ -115,8 +115,8 @@ class VPMeshGmsh: found_an_analysis = False for o in gui_doc.Document.Objects: if o.isDerivedFrom("Fem::FemAnalysisPython"): - found_an_analysis = True - break + found_an_analysis = True + break if found_an_analysis: if FemGui.getActiveAnalysis() is not None: if FemGui.getActiveAnalysis().Document is FreeCAD.ActiveDocument: @@ -206,12 +206,20 @@ class VPMeshGmsh: children = self.claimChildren() if len(children) > 0: # issue a warning - bodyMessage = "The mesh contains submesh objects, therefore the\nfollowing referencing objects might be lost:\n" + message_text = ( + "The mesh contains submesh objects, therefore the\n" + "following referencing objects might be lost:\n" + ) for obj in children: - bodyMessage += "\n" + obj.Label - bodyMessage += "\n\nAre you sure you want to continue?" - reply = QtGui.QMessageBox.warning(None, "Object dependencies", bodyMessage, - QtGui.QMessageBox.Yes | QtGui.QMessageBox.No, QtGui.QMessageBox.No) + message_text += "\n" + obj.Label + message_text += "\n\nAre you sure you want to continue?" + reply = QtGui.QMessageBox.warning( + None, + "Object dependencies", + message_text, + QtGui.QMessageBox.Yes | QtGui.QMessageBox.No, + QtGui.QMessageBox.No + ) if reply == QtGui.QMessageBox.Yes: return True else: diff --git a/src/Mod/Fem/femviewprovider/view_result_mechanical.py b/src/Mod/Fem/femviewprovider/view_result_mechanical.py index 9565b93797..b8bcdcf577 100644 --- a/src/Mod/Fem/femviewprovider/view_result_mechanical.py +++ b/src/Mod/Fem/femviewprovider/view_result_mechanical.py @@ -30,7 +30,6 @@ __url__ = "https://www.freecadweb.org" # \ingroup FEM # \brief view provider for mechanical ResultObjectPython -import FreeCAD import FreeCADGui from PySide import QtGui @@ -63,16 +62,24 @@ class VPResultMechanical(view_base_femconstraint.VPBaseFemConstraint): def onDelete(self, feature, subelements): children = self.claimChildren() - filtered = filter(lambda obj: not obj is None, children) + filtered = filter(lambda obj: obj is not None, children) children = list(filtered) if len(children) > 0: # issue a warning - bodyMessage = "The results object is not empty, therefore the\nfollowing referencing objects might be lost:\n" + bodyMessage = ( + "The results object is not empty, therefore the\n" + "following referencing objects might be lost:\n" + ) for obj in children: bodyMessage += "\n" + obj.Label bodyMessage += "\n\nAre you sure you want to continue?" - reply = QtGui.QMessageBox.warning(None, "Object dependencies", bodyMessage, - QtGui.QMessageBox.Yes | QtGui.QMessageBox.No, QtGui.QMessageBox.No) + reply = QtGui.QMessageBox.warning( + None, + "Object dependencies", + bodyMessage, + QtGui.QMessageBox.Yes | QtGui.QMessageBox.No, + QtGui.QMessageBox.No + ) if reply == QtGui.QMessageBox.Yes: return True else: diff --git a/src/Mod/Part/App/GeomPlate/PointConstraintPyImp.cpp b/src/Mod/Part/App/GeomPlate/PointConstraintPyImp.cpp index dee67db571..73e9284a5f 100644 --- a/src/Mod/Part/App/GeomPlate/PointConstraintPyImp.cpp +++ b/src/Mod/Part/App/GeomPlate/PointConstraintPyImp.cpp @@ -22,6 +22,7 @@ #include "PreCompiled.h" #ifndef _PreComp_ +# include # include #endif diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index be114c7e10..c6899e5e54 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -142,6 +142,9 @@ SET(PathScripts_SRCS PathScripts/PathWaterline.py PathScripts/PathWaterlineGui.py PathScripts/PostUtils.py + PathScripts/PostUtilsArguments.py + PathScripts/PostUtilsExport.py + PathScripts/PostUtilsParse.py PathScripts/__init__.py ) @@ -174,6 +177,11 @@ SET(PathScripts_post_SRCS PathScripts/post/opensbp_post.py PathScripts/post/opensbp_pre.py PathScripts/post/philips_post.py + PathScripts/post/refactored_centroid_post.py + PathScripts/post/refactored_grbl_post.py + PathScripts/post/refactored_linuxcnc_post.py + PathScripts/post/refactored_mach3_mach4_post.py + PathScripts/post/refactored_test_post.py PathScripts/post/rml_post.py PathScripts/post/rrf_post.py PathScripts/post/slic3r_pre.py @@ -221,14 +229,19 @@ SET(Tools_Shape_SRCS SET(PathTests_SRCS PathTests/__init__.py PathTests/boxtest.fcstd + PathTests/boxtest1.fcstd PathTests/Drilling_1.FCStd + PathTests/drill_test1.FCStd PathTests/PathTestUtils.py PathTests/test_adaptive.fcstd PathTests/test_centroid_00.ngc PathTests/test_filenaming.fcstd PathTests/test_geomop.fcstd PathTests/test_holes00.fcstd - PathTests/test_linuxcnc_00.ngc + PathTests/TestCentroidPost.py + PathTests/TestGrblPost.py + PathTests/TestLinuxCNCPost.py + PathTests/TestMach3Mach4Post.py PathTests/TestPathAdaptive.py PathTests/TestPathCore.py PathTests/TestPathDeburr.py @@ -259,6 +272,11 @@ SET(PathTests_SRCS PathTests/TestPathUtil.py PathTests/TestPathVcarve.py PathTests/TestPathVoronoi.py + PathTests/TestRefactoredCentroidPost.py + PathTests/TestRefactoredGrblPost.py + PathTests/TestRefactoredLinuxCNCPost.py + PathTests/TestRefactoredMach3Mach4Post.py + PathTests/TestRefactoredTestPost.py PathTests/Tools/Bit/test-path-tool-bit-bit-00.fctb PathTests/Tools/Library/test-path-tool-bit-library-00.fctl PathTests/Tools/Shape/test-path-tool-bit-shape-00.fcstd @@ -413,4 +431,3 @@ INSTALL( DESTINATION Mod/Path/Data/Threads ) - diff --git a/src/Mod/Path/PathScripts/PathPost.py b/src/Mod/Path/PathScripts/PathPost.py index 9062f31ebb..f185678151 100644 --- a/src/Mod/Path/PathScripts/PathPost.py +++ b/src/Mod/Path/PathScripts/PathPost.py @@ -59,51 +59,15 @@ class _TempObject: Label = "Fixture" -def resolveFileName(job, subpartname, sequencenumber): - PathLog.track(subpartname, sequencenumber) - - validPathSubstitutions = ["D", "d", "M", "j"] - validFilenameSubstitutions = ["j", "d", "T", "t", "W", "O", "S"] - - # Look for preference default - outputpath, filename = os.path.split(PathPreferences.defaultOutputFile()) - filename, ext = os.path.splitext(filename) - - # Override with document default if it exists - if job.PostProcessorOutputFile: - matchstring = job.PostProcessorOutputFile - candidateOutputPath, candidateFilename = os.path.split(matchstring) - - if candidateOutputPath: - outputpath = candidateOutputPath - - if candidateFilename: - filename, ext = os.path.splitext(candidateFilename) - - # Strip any invalid substitutions from the ouputpath - for match in re.findall("%(.)", outputpath): - if match not in validPathSubstitutions: - outputpath = outputpath.replace(f"%{match}", "") - - # if nothing else, use current directory - if not outputpath: - outputpath = "." - - # Strip any invalid substitutions from the filename - for match in re.findall("%(.)", filename): - if match not in validFilenameSubstitutions: - filename = filename.replace(f"%{match}", "") - - # if no filename, use the active document label - if not filename: - filename = FreeCAD.ActiveDocument.Label - - # if no extension, use something sensible - if not ext: - ext = ".nc" - - # By now we should have a sanitized path, filename and extension to work with - PathLog.track(f"path: {outputpath} name: {filename} ext: {ext}") +def processFileNameSubstitutions( + job, + subpartname, + sequencenumber, + outputpath, + filename, + ext, +): + """Process any substitutions in the outputpath or filename.""" # The following section allows substitution within the path part PathLog.track(f"path before substitution: {outputpath}") @@ -187,13 +151,71 @@ def resolveFileName(job, subpartname, sequencenumber): fullPath = f"{outputpath}{os.path.sep}{filename}{ext}" PathLog.track(f"full filepath: {fullPath}") + return fullPath + + +def resolveFileName(job, subpartname, sequencenumber): + PathLog.track(subpartname, sequencenumber) + + validPathSubstitutions = ["D", "d", "M", "j"] + validFilenameSubstitutions = ["j", "d", "T", "t", "W", "O", "S"] + + # Look for preference default + outputpath, filename = os.path.split(PathPreferences.defaultOutputFile()) + filename, ext = os.path.splitext(filename) + + # Override with document default if it exists + if job.PostProcessorOutputFile: + matchstring = job.PostProcessorOutputFile + candidateOutputPath, candidateFilename = os.path.split(matchstring) + + if candidateOutputPath: + outputpath = candidateOutputPath + + if candidateFilename: + filename, ext = os.path.splitext(candidateFilename) + + # Strip any invalid substitutions from the ouputpath + for match in re.findall("%(.)", outputpath): + if match not in validPathSubstitutions: + outputpath = outputpath.replace(f"%{match}", "") + + # if nothing else, use current directory + if not outputpath: + outputpath = "." + + # Strip any invalid substitutions from the filename + for match in re.findall("%(.)", filename): + if match not in validFilenameSubstitutions: + filename = filename.replace(f"%{match}", "") + + # if no filename, use the active document label + if not filename: + filename = FreeCAD.ActiveDocument.Label + + # if no extension, use something sensible + if not ext: + ext = ".nc" + + # By now we should have a sanitized path, filename and extension to work with + PathLog.track(f"path: {outputpath} name: {filename} ext: {ext}") + + fullPath = processFileNameSubstitutions( + job, + subpartname, + sequencenumber, + outputpath, + filename, + ext, + ) # This section determines whether user interaction is necessary policy = PathPreferences.defaultOutputPolicy() openDialog = policy == "Open File Dialog" # if os.path.isdir(filename) or not os.path.isdir(os.path.dirname(filename)): - # # Either the entire filename resolves into a directory or the parent directory doesn't exist. + # # Either the entire filename resolves into a directory or the parent + # # directory doesn't exist. # # Either way I don't know what to do - ask for help # openDialog = True @@ -235,7 +257,7 @@ def resolveFileName(job, subpartname, sequencenumber): def buildPostList(job): """Takes the job and determines the specific objects and order to postprocess Returns a list of objects which can be passed to - exportObjectsWith() for final posting""" + exportObjectsWith() for final posting.""" wcslist = job.Fixtures orderby = job.OrderOutputBy @@ -243,8 +265,8 @@ def buildPostList(job): if orderby == "Fixture": PathLog.debug("Ordering by Fixture") - # Order by fixture means all operations and tool changes will be completed in one - # fixture before moving to the next. + # Order by fixture means all operations and tool changes will be + # completed in one fixture before moving to the next. currTool = None for index, f in enumerate(wcslist): diff --git a/src/Mod/Path/PathScripts/PathPostProcessor.py b/src/Mod/Path/PathScripts/PathPostProcessor.py index c692032ba1..85857c2394 100644 --- a/src/Mod/Path/PathScripts/PathPostProcessor.py +++ b/src/Mod/Path/PathScripts/PathPostProcessor.py @@ -69,23 +69,6 @@ class PostProcessor: else: instance.units = "Inch" - if hasattr(current_post, "MACHINE_NAME"): - instance.machineName = current_post.MACHINE_NAME - - if hasattr(current_post, "CORNER_MAX"): - instance.cornerMax = { - "x": current_post.CORNER_MAX["x"], - "y": current_post.CORNER_MAX["y"], - "z": current_post.CORNER_MAX["z"], - } - - if hasattr(current_post, "CORNER_MIN"): - instance.cornerMin = { - "x": current_post.CORNER_MIN["x"], - "y": current_post.CORNER_MIN["y"], - "z": current_post.CORNER_MIN["z"], - } - if hasattr(current_post, "TOOLTIP"): instance.tooltip = current_post.TOOLTIP if hasattr(current_post, "TOOLTIP_ARGS"): @@ -96,8 +79,6 @@ class PostProcessor: self.script = script self.tooltip = None self.tooltipArgs = None - self.cornerMax = None - self.cornerMin = None self.units = None self.machineName = None diff --git a/src/Mod/Path/PathScripts/PostUtils.py b/src/Mod/Path/PathScripts/PostUtils.py index c6a4aa36bd..96e9d6c831 100644 --- a/src/Mod/Path/PathScripts/PostUtils.py +++ b/src/Mod/Path/PathScripts/PostUtils.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # *************************************************************************** # * Copyright (c) 2014 Yorik van Havre * +# * Copyright (c) 2022 Larry Woestman * # * * # * This file is part of the FreeCAD CAx development system. * # * * @@ -23,18 +24,20 @@ # *************************************************************************** """ -These are a common functions and classes for creating custom post processors. +These are common functions and classes for creating custom post processors. """ from PySide import QtCore, QtGui + import FreeCAD -from PathMachineState import MachineState + import Path import Part + +from PathMachineState import MachineState from PathScripts.PathGeom import CmdMoveArc, edgeForCmd, cmdsForEdge translate = FreeCAD.Qt.translate - FreeCADGui = None if FreeCAD.GuiUp: import FreeCADGui @@ -147,7 +150,7 @@ def stringsplit(commandline): def fmt(num, dec, units): - """used to format axis moves, feedrate, etc for decimal places and units""" + """Use to format axis moves, feedrate, etc for decimal places and units.""" if units == "G21": # metric fnum = "%.*f" % (dec, num) else: # inch @@ -156,8 +159,7 @@ def fmt(num, dec, units): def editor(gcode): - """pops up a handy little editor to look at the code output""" - + """Pops up a handy little editor to look at the code output.""" prefs = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Path") # default Max Highlighter Size = 512 Ko defaultMHS = 512 * 1024 @@ -190,7 +192,7 @@ def editor(gcode): def fcoms(string, commentsym): - """filter and rebuild comments with user preferred comment symbol""" + """Filter and rebuild comments with user preferred comment symbol.""" if len(commentsym) == 1: s1 = string.replace("(", commentsym) comment = s1.replace(")", "") @@ -200,8 +202,10 @@ def fcoms(string, commentsym): def splitArcs(path): - """filters a path object and replaces at G2/G3 moves with discrete G1 - returns a Path object""" + """Filter a path object and replace all G2/G3 moves with discrete G1 moves. + + Returns a Path object. + """ prefGrp = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Path") deflection = prefGrp.GetFloat("LibAreaCurveAccuarcy", 0.01) diff --git a/src/Mod/Path/PathScripts/PostUtilsArguments.py b/src/Mod/Path/PathScripts/PostUtilsArguments.py new file mode 100644 index 0000000000..df0895ccda --- /dev/null +++ b/src/Mod/Path/PathScripts/PostUtilsArguments.py @@ -0,0 +1,655 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2014 Yorik van Havre * +# * Copyright (c) 2014 sliptonic * +# * Copyright (c) 2015 Dan Falck * +# * Copyright (c) 2018, 2019 Gauthier Briere * +# * Copyright (c) 2019, 2020 Schildkroet * +# * Copyright (c) 2022 Larry Woestman * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * 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. * +# * * +# * FreeCAD 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 Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +""" +These are functions related to arguments and values for creating custom post processors. +""" +import argparse +import os +import shlex + + +def add_flag_type_arguments( + argument_group, + default_flag, + true_argument, + false_argument, + true_help, + false_help, + visible=True, +): + if visible: + if default_flag: + true_help += " (default)" + else: + false_help += " (default)" + else: + true_help = false_help = argparse.SUPPRESS + argument_group.add_argument(true_argument, action="store_true", help=true_help) + argument_group.add_argument(false_argument, action="store_true", help=false_help) + + +def init_argument_defaults(argument_defaults): + """Initialize which argument to show as the default in flag-type arguments.""" + argument_defaults["axis-modal"] = False + argument_defaults["bcnc"] = False + argument_defaults["comments"] = True + argument_defaults["header"] = True + argument_defaults["line-numbers"] = False + argument_defaults["metric_inches"] = True + argument_defaults["modal"] = False + argument_defaults["show-editor"] = True + argument_defaults["tlo"] = True + argument_defaults["tool_change"] = True + argument_defaults["translate_drill"] = False + + +def init_arguments_visible(arguments_visible): + """Initialize the flags for which arguments are visible in the arguments tooltip.""" + arguments_visible["bcnc"] = False + arguments_visible["axis-modal"] = True + arguments_visible["axis-precision"] = True + arguments_visible["comments"] = True + arguments_visible["feed-precision"] = True + arguments_visible["header"] = True + arguments_visible["line-numbers"] = True + arguments_visible["metric_inches"] = True + arguments_visible["modal"] = True + arguments_visible["postamble"] = True + arguments_visible["preamble"] = True + arguments_visible["precision"] = True + arguments_visible["return-to"] = False + arguments_visible["show-editor"] = True + arguments_visible["tlo"] = True + arguments_visible["tool_change"] = False + arguments_visible["translate_drill"] = False + arguments_visible["wait-for-spindle"] = False + + +def init_shared_arguments(values, argument_defaults, arguments_visible): + """Initialize the shared arguments for postprocessors.""" + parser = argparse.ArgumentParser( + prog=values["MACHINE_NAME"], usage=argparse.SUPPRESS, add_help=False + ) + shared = parser.add_argument_group( + "Arguments that are shared with all postprocessors" + ) + add_flag_type_arguments( + shared, + argument_defaults["metric_inches"], + "--metric", + "--inches", + "Convert output for Metric mode (G21)", + "Convert output for US imperial mode (G20)", + arguments_visible["metric_inches"], + ) + add_flag_type_arguments( + shared, + argument_defaults["axis-modal"], + "--axis-modal", + "--no-axis-modal", + "Don't output axis values if they are the same as the previous line", + "Output axis values even if they are the same as the previous line", + arguments_visible["axis-modal"], + ) + if arguments_visible["axis-precision"]: + help_message = ( + "Number of digits of precision for axis moves, default is " + + str(values["DEFAULT_AXIS_PRECISION"]) + ) + else: + help_message = argparse.SUPPRESS + shared.add_argument( + "--axis-precision", + default=-1, + type=int, + help=help_message, + ) + add_flag_type_arguments( + shared, + argument_defaults["bcnc"], + "--bcnc", + "--no-bcnc", + "Add Job operations as bCNC block headers. Consider suppressing comments by adding --no-comments", + "Suppress bCNC block header output", + arguments_visible["bcnc"], + ) + add_flag_type_arguments( + shared, + argument_defaults["comments"], + "--comments", + "--no-comments", + "Output comments", + "Suppress comment output", + arguments_visible["comments"], + ) + if arguments_visible["feed-precision"]: + help_message = "Number of digits of precision for feed rate, default is " + str( + values["DEFAULT_FEED_PRECISION"] + ) + else: + help_message = argparse.SUPPRESS + shared.add_argument( + "--feed-precision", + default=-1, + type=int, + help=help_message, + ) + add_flag_type_arguments( + shared, + argument_defaults["header"], + "--header", + "--no-header", + "Output headers", + "Suppress header output", + arguments_visible["header"], + ) + add_flag_type_arguments( + shared, + argument_defaults["line-numbers"], + "--line-numbers", + "--no-line-numbers", + "Prefix with line numbers", + "Don't prefix with line numbers", + arguments_visible["line-numbers"], + ) + add_flag_type_arguments( + shared, + argument_defaults["modal"], + "--modal", + "--no-modal", + "Don't output the G-command name if it is the same as the previous line", + "Output the G-command name even if it is the same as the previous line", + arguments_visible["modal"], + ) + if arguments_visible["postamble"]: + help_message = ( + 'Set commands to be issued after the last command, default is "' + + values["POSTAMBLE"] + + '"' + ) + else: + help_message = argparse.SUPPRESS + shared.add_argument("--postamble", help=help_message) + if arguments_visible["preamble"]: + help_message = ( + 'Set commands to be issued before the first command, default is "' + + values["PREAMBLE"] + + '"' + ) + else: + help_message = argparse.SUPPRESS + shared.add_argument("--preamble", help=help_message) + # The --precision argument is included for backwards compatibility with + # some postprocessors. If both --axis-precision and --precision are provided, + # the --axis-precision value "wins". If both --feed-precision and --precision + # are provided, the --feed-precision value "wins". + if arguments_visible["precision"]: + help_message = ( + "Number of digits of precision for both feed rate and axis moves, default is " + + str(values["DEFAULT_AXIS_PRECISION"]) + + " for metric or " + + str(values["DEFAULT_INCH_AXIS_PRECISION"]) + + " for inches" + ) + else: + help_message = argparse.SUPPRESS + shared.add_argument( + "--precision", + default=-1, + type=int, + help=help_message, + ) + if arguments_visible["return-to"]: + help_message = "Move to the specified x,y,z coordinates at the end, e.g. --return-to=0,0,0 (default is do not move)" + else: + help_message = argparse.SUPPRESS + shared.add_argument("--return-to", default="", help=help_message) + add_flag_type_arguments( + shared, + argument_defaults["show-editor"], + "--show-editor", + "--no-show-editor", + "Pop up editor before writing output", + "Don't pop up editor before writing output", + arguments_visible["show-editor"], + ) + add_flag_type_arguments( + shared, + argument_defaults["tlo"], + "--tlo", + "--no-tlo", + "Output tool length offset (G43) following tool changes", + "Suppress tool length offset (G43) following tool changes", + arguments_visible["tlo"], + ) + add_flag_type_arguments( + shared, + argument_defaults["tool_change"], + "--tool_change", + "--no-tool_change", + "Insert M6 and any other tool change G-code for all tool changes", + "Convert M6 to a comment for all tool changes", + arguments_visible["tool_change"], + ) + add_flag_type_arguments( + shared, + argument_defaults["translate_drill"], + "--translate_drill", + "--no-translate_drill", + "Translate drill cycles G81, G82 & G83 into G0/G1 movements", + "Don't translate drill cycles G81, G82 & G83 into G0/G1 movements", + arguments_visible["translate_drill"], + ) + if arguments_visible["wait-for-spindle"]: + help_message = "Time to wait (in seconds) after M3, M4 (default = 0.0)" + else: + help_message = argparse.SUPPRESS + shared.add_argument( + "--wait-for-spindle", type=float, default=0.0, help=help_message + ) + return parser + + +def init_shared_values(values): + """Initialize the default values in postprocessors.""" + # + # The starting axis precision is 3 digits after the decimal point. + # + values["AXIS_PRECISION"] = 3 + # + # If this is set to "", all spaces are removed from between commands and parameters. + # + values["COMMAND_SPACE"] = " " + # + # The character that indicates a comment. While "(" is the most common, + # ";" is also used. + # + values["COMMENT_SYMBOL"] = "(" + # + # Variables storing the current position for the drill_translate routine. + # + values["CURRENT_X"] = 0 + values["CURRENT_Y"] = 0 + values["CURRENT_Z"] = 0 + # + # Default axis precision for metric is 3 digits after the decimal point. + # (see http://linuxcnc.org/docs/2.7/html/gcode/overview.html#_g_code_best_practices) + # + values["DEFAULT_AXIS_PRECISION"] = 3 + # + # The default precision for feed is also set to 3 for metric. + # + values["DEFAULT_FEED_PRECISION"] = 3 + # + # Default axis precision for inch/imperial is 4 digits after the decimal point. + # + values["DEFAULT_INCH_AXIS_PRECISION"] = 4 + # + # The default precision for feed is also set to 4 for inch/imperial. + # + values["DEFAULT_INCH_FEED_PRECISION"] = 4 + # + # If TRANSLATE_DRILL_CYCLES is True, these are the drill cycles + # that get translated to G0 and G1 commands. + # + values["DRILL_CYCLES_TO_TRANSLATE"] = ("G81", "G82", "G83") + # + # The default value of drill retractations (CURRENT_Z). + # The other possible value is G99. + # + values["DRILL_RETRACT_MODE"] = "G98" + # + # If this is set to True, then M7, M8, and M9 commands + # to enable/disable coolant will be output. + # + values["ENABLE_COOLANT"] = False + # + # If this is set to True, then commands that are placed in + # comments that look like (MC_RUN_COMMAND: blah) will be output. + # + values["ENABLE_MACHINE_SPECIFIC_COMMANDS"] = False + # + # By default the line ending characters of the output file(s) + # are written to match the system that the postprocessor runs on. + # If you need to force the line ending characters to a specific + # value, set this variable to "\n" or "\r\n" instead. + # + values["END_OF_LINE_CHARACTERS"] = os.linesep + # + # The starting precision for feed is also set to 3 digits after the decimal point. + # + values["FEED_PRECISION"] = 3 + # + # This value shows up in the post_op comment as "Finish operation:". + # At least one postprocessor changes it to "End" to produce "End operation:". + # + values["FINISH_LABEL"] = "Finish" + # + # The name of the machine the postprocessor is for + # + values["MACHINE_NAME"] = "unknown machine" + # + # The line number increment value + # + values["LINE_INCREMENT"] = 10 + # + # The line number starting value + # + values["line_number"] = 100 + # + # If this value is True, then a list of tool numbers + # with their labels are output just before the preamble. + # + values["LIST_TOOLS_IN_PREAMBLE"] = False + # + # If this value is true G-code commands are suppressed if they are + # the same as the previous line. + # + values["MODAL"] = False + # + # This defines the motion commands that might change the X, Y, and Z position. + # + values["MOTION_COMMANDS"] = [ + "G0", + "G00", + "G1", + "G01", + "G2", + "G02", + "G3", + "G03", + ] + # + # Keeps track of the motion mode currently in use. + # G90 for absolute moves, G91 for relative + # + values["MOTION_MODE"] = "G90" + # + # If True enables special processing for operations with "Adaptive" in the name + # + values["OUTPUT_ADAPTIVE"] = False + # + # If True adds bCNC operation block headers to the output G-code file. + # + values["OUTPUT_BCNC"] = False + # + # If True output comments. If False comments are suppressed. + # + values["OUTPUT_COMMENTS"] = True + # + # if False duplicate axis values are suppressed if they are the same as + # the previous line. + # + values["OUTPUT_DOUBLES"] = True + # + # If True output the machine name in the pre_op + # + values["OUTPUT_MACHINE_NAME"] = False + # + # If True output a header at the front of the G-code file. + # The header contains comments similar to: + # (Exported by FreeCAD) + # (Post Processor: centroid_post) + # (Cam File: box.fcstd) + # (Output Time:2020-01-01 01:02:03.123456) + # + values["OUTPUT_HEADER"] = True + # + # If True output line numbers at the front of each line. + # If False do not output line numbers. + # + values["OUTPUT_LINE_NUMBERS"] = False + # + # If True output Path labels at the beginning of each Path. + # + values["OUTPUT_PATH_LABELS"] = False + # + # If True output tool change G-code for M6 commands followed + # by any commands in the "TOOL_CHANGE" value. + # If False output the M6 command as a comment and do not output + # any commands in the "TOOL_CHANGE" value. + # + values["OUTPUT_TOOL_CHANGE"] = True + # + # This list controls the order of parameters in a line during output. + # + values["PARAMETER_ORDER"] = [ + "X", + "Y", + "Z", + "A", + "B", + "C", + "U", + "V", + "W", + "I", + "J", + "K", + "F", + "S", + "T", + "Q", + "R", + "L", + "P", + ] + # + # Any commands in this value will be output as the last commands + # in the G-code file. + # + values["POSTAMBLE"] = """""" + # + # Any commands in this value will be output after the operation(s). + # + values["POST_OPERATION"] = """""" + # + # Any commands in this value will be output after the header and + # safety block at the beginning of the G-code file. + # + values["PREAMBLE"] = """""" + # + # Any commands in this value will be output before the operation(s). + # + values["PRE_OPERATION"] = """""" + # + # Defines which G-code commands are considered "rapid" moves. + # + values["RAPID_MOVES"] = ["G0", "G00"] + # + # If True suppress any messages. + # + values["REMOVE_MESSAGES"] = True + # + # Any commands in this value are output after the operation(s) + # and post_operation commands are output but before the + # TOOLRETURN, SAFETYBLOCK, and POSTAMBLE. + # + values["RETURN_TO"] = None + # + # Any commands in this value are output after the header but before the preamble, + # then again after the TOOLRETURN but before the POSTAMBLE. + # + values["SAFETYBLOCK"] = """""" + # + # If True then the G-code editor widget is shown before writing + # the G-code to the file. + # + values["SHOW_EDITOR"] = True + # + # If True then the current machine units are output just before the PRE_OPERATION. + # + values["SHOW_MACHINE_UNITS"] = True + # + # If True then the current operation label is output just before the PRE_OPERATION. + # + values["SHOW_OPERATION_LABELS"] = True + # + # The number of decimal places to use when outputting the speed (S) parameter. + # + values["SPINDLE_DECIMALS"] = 0 + # + # The amount of time (in seconds) to wait after turning on the spindle + # using an M3 or M4 command (a floating point number). + # + values["SPINDLE_WAIT"] = 0.0 + # + # If true then then an M5 command to stop the spindle is output + # after the M6 tool change command and before the TOOL_CHANGE commands. + # + values["STOP_SPINDLE_FOR_TOOL_CHANGE"] = True + # + # These commands are ignored by commenting them out. + # Used when replacing the drill commands by G0 and G1 commands, for example. + # + values["SUPPRESS_COMMANDS"] = [] + # + # Any commands in this value are output after the M6 command + # when changing at tool (if OUTPUT_TOOL_CHANGE is True). + # + values["TOOL_CHANGE"] = """""" + # + # Any commands in this value are output after the POST_OPERATION, + # RETURN_TO, and OUTPUT_BCNC and before the SAFETYBLOCK and POSTAMBLE. + # + values["TOOLRETURN"] = """""" + # + # If true, G81, G82 & G83 drill moves are translated into G0/G1 moves. + # + values["TRANSLATE_DRILL_CYCLES"] = False + # + # These values keep track of whether we are in Metric mode (G21) + # or inches/imperial mode (G20). + # + values["UNITS"] = "G21" + values["UNIT_FORMAT"] = "mm" + values["UNIT_SPEED_FORMAT"] = "mm/min" + # + # If true a tool length command (G43) will be output following tool changes. + # + values["USE_TLO"] = True + + +def process_shared_arguments(values, parser, argstring): + """Process the arguments to the postprocessor.""" + try: + args = parser.parse_args(shlex.split(argstring)) + if args.metric: + values["UNITS"] = "G21" + if args.inches: + values["UNITS"] = "G20" + if values["UNITS"] == "G21": + values["UNIT_FORMAT"] = "mm" + values["UNIT_SPEED_FORMAT"] = "mm/min" + if values["UNITS"] == "G20": + values["UNIT_FORMAT"] = "in" + values["UNIT_SPEED_FORMAT"] = "in/min" + # The precision-related arguments need to be processed + # after the metric/inches arguments are processed. + # If both --axis-precision and --precision are given, + # the --axis-precision argument "wins". + if args.axis_precision != -1: + values["AXIS_PRECISION"] = args.axis_precision + elif args.precision != -1: + values["AXIS_PRECISION"] = args.precision + else: + if values["UNITS"] == "G21": + values["AXIS_PRECISION"] = values["DEFAULT_AXIS_PRECISION"] + if values["UNITS"] == "G20": + values["AXIS_PRECISION"] = values["DEFAULT_INCH_AXIS_PRECISION"] + # If both --feed-precision and --precision are given, + # the --feed-precision argument "wins". + if args.feed_precision != -1: + values["FEED_PRECISION"] = args.feed_precision + elif args.precision != -1: + values["FEED_PRECISION"] = args.precision + else: + if values["UNITS"] == "G21": + values["FEED_PRECISION"] = values["DEFAULT_FEED_PRECISION"] + if values["UNITS"] == "G20": + values["FEED_PRECISION"] = values["DEFAULT_INCH_FEED_PRECISION"] + if args.axis_modal: + values["OUTPUT_DOUBLES"] = False + if args.no_axis_modal: + values["OUTPUT_DOUBLES"] = True + if args.bcnc: + values["OUTPUT_BCNC"] = True + if args.no_bcnc: + values["OUTPUT_BCNC"] = False + if args.comments: + values["OUTPUT_COMMENTS"] = True + if args.no_comments: + values["OUTPUT_COMMENTS"] = False + if args.header: + values["OUTPUT_HEADER"] = True + if args.no_header: + values["OUTPUT_HEADER"] = False + if args.line_numbers: + values["OUTPUT_LINE_NUMBERS"] = True + if args.no_line_numbers: + values["OUTPUT_LINE_NUMBERS"] = False + if args.modal: + values["MODAL"] = True + if args.no_modal: + values["MODAL"] = False + if args.postamble is not None: + values["POSTAMBLE"] = args.postamble + if args.preamble is not None: + values["PREAMBLE"] = args.preamble + if args.return_to != "": + values["RETURN_TO"] = [int(v) for v in args.return_to.split(",")] + if len(values["RETURN_TO"]) != 3: + values["RETURN_TO"] = None + print( + "--return-to coordinates must be specified as ,,, ignoring" + ) + if args.show_editor: + values["SHOW_EDITOR"] = True + if args.no_show_editor: + values["SHOW_EDITOR"] = False + if args.tlo: + values["USE_TLO"] = True + if args.no_tlo: + values["USE_TLO"] = False + if args.tool_change: + values["OUTPUT_TOOL_CHANGE"] = True + if args.no_tool_change: + values["OUTPUT_TOOL_CHANGE"] = False + if args.translate_drill: + values["TRANSLATE_DRILL_CYCLES"] = True + if args.no_translate_drill: + values["TRANSLATE_DRILL_CYCLES"] = False + if args.wait_for_spindle > 0.0: + values["SPINDLE_WAIT"] = args.wait_for_spindle + + except Exception: + return (False, None) + + return (True, args) diff --git a/src/Mod/Path/PathScripts/PostUtilsExport.py b/src/Mod/Path/PathScripts/PostUtilsExport.py new file mode 100644 index 0000000000..9c9263f8af --- /dev/null +++ b/src/Mod/Path/PathScripts/PostUtilsExport.py @@ -0,0 +1,313 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2014 Yorik van Havre * +# * Copyright (c) 2014 sliptonic * +# * Copyright (c) 2015 Dan Falck * +# * Copyright (c) 2018, 2019 Gauthier Briere * +# * Copyright (c) 2019, 2020 Schildkroet * +# * Copyright (c) 2022 Larry Woestman * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * 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. * +# * * +# * FreeCAD 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 Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +import datetime +import os + +import FreeCAD + +from PathScripts import PathToolController +from PathScripts import PostUtils +from PathScripts import PostUtilsParse + + +# to distinguish python built-in open function from the one declared below +if open.__module__ in ["__builtin__", "io"]: + pythonopen = open + +# +# This routine processes things in the following order: +# +# OUTPUT_HEADER +# SAFETYBLOCK +# LIST_TOOLS_IN_PREAMBLE +# PREAMBLE +# OUTPUT_BCNC +# SHOW_OPERATION_LABELS +# SHOW_MACHINE_UNITS +# PRE_OPERATION +# ENABLE_COOLANT (coolant on) +# operation(s) +# POST_OPERATION +# ENABLE_COOLANT (coolant off) +# RETURN_TO +# OUTPUT_BCNC +# TOOLRETURN +# SAFETYBLOCK +# POSTAMBLE +# SHOW_EDITOR +# +# The names in all caps may be enabled/disabled/modified by setting +# the corresponding value in the postprocessor. +# + + +def export_common(values, objectslist, filename): + """Do the common parts of postprocessing the objects in objectslist to filename.""" + # + for obj in objectslist: + if not hasattr(obj, "Path"): + print( + "The object " + + obj.Name + + " is not a path. Please select only path and Compounds." + ) + return None + + # for obj in objectslist: + # print(obj.Name) + + print("PostProcessor: " + values["POSTPROCESSOR_FILE_NAME"] + " postprocessing...") + gcode = "" + + # write header + if values["OUTPUT_HEADER"]: + comment = PostUtilsParse.create_comment( + "Exported by FreeCAD", values["COMMENT_SYMBOL"] + ) + gcode += PostUtilsParse.linenumber(values) + comment + "\n" + comment = PostUtilsParse.create_comment( + "Post Processor: " + values["POSTPROCESSOR_FILE_NAME"], + values["COMMENT_SYMBOL"], + ) + gcode += PostUtilsParse.linenumber(values) + comment + "\n" + if FreeCAD.ActiveDocument: + cam_file = os.path.basename(FreeCAD.ActiveDocument.FileName) + else: + cam_file = "" + comment = PostUtilsParse.create_comment( + "Cam File: " + cam_file, values["COMMENT_SYMBOL"] + ) + gcode += PostUtilsParse.linenumber(values) + comment + "\n" + comment = PostUtilsParse.create_comment( + "Output Time: " + str(datetime.datetime.now()), values["COMMENT_SYMBOL"] + ) + gcode += PostUtilsParse.linenumber(values) + comment + "\n" + + # Check canned cycles for drilling + if values["TRANSLATE_DRILL_CYCLES"]: + if len(values["SUPPRESS_COMMANDS"]) == 0: + values["SUPPRESS_COMMANDS"] = ["G99", "G98", "G80"] + else: + values["SUPPRESS_COMMANDS"] += ["G99", "G98", "G80"] + + for line in values["SAFETYBLOCK"].splitlines(False): + gcode += PostUtilsParse.linenumber(values) + line + "\n" + + # Write the preamble + if values["OUTPUT_COMMENTS"]: + if values["LIST_TOOLS_IN_PREAMBLE"]: + for item in objectslist: + if hasattr(item, "Proxy") and isinstance( + item.Proxy, PathToolController.ToolController + ): + comment = PostUtilsParse.create_comment( + "T{}={}".format(item.ToolNumber, item.Name), + values["COMMENT_SYMBOL"], + ) + gcode += PostUtilsParse.linenumber(values) + comment + "\n" + comment = PostUtilsParse.create_comment( + "Begin preamble", values["COMMENT_SYMBOL"] + ) + gcode += PostUtilsParse.linenumber(values) + comment + "\n" + for line in values["PREAMBLE"].splitlines(False): + gcode += PostUtilsParse.linenumber(values) + line + "\n" + # verify if PREAMBLE or SAFETYBLOCK have changed MOTION_MODE or UNITS + if "G90" in values["PREAMBLE"] or "G90" in values["SAFETYBLOCK"]: + values["MOTION_MODE"] = "G90" + elif "G91" in values["PREAMBLE"] or "G91" in values["SAFETYBLOCK"]: + values["MOTION_MODE"] = "G91" + else: + gcode += PostUtilsParse.linenumber(values) + values["MOTION_MODE"] + "\n" + if "G21" in values["PREAMBLE"] or "G21" in values["SAFETYBLOCK"]: + values["UNITS"] = "G21" + values["UNIT_FORMAT"] = "mm" + values["UNIT_SPEED_FORMAT"] = "mm/min" + elif "G20" in values["PREAMBLE"] or "G20" in values["SAFETYBLOCK"]: + values["UNITS"] = "G20" + values["UNIT_FORMAT"] = "in" + values["UNIT_SPEED_FORMAT"] = "in/min" + else: + gcode += PostUtilsParse.linenumber(values) + values["UNITS"] + "\n" + + for obj in objectslist: + + # Debug... + # print("\n" + "*"*70) + # dump(obj) + # print("*"*70 + "\n") + + # Skip inactive operations + if hasattr(obj, "Active"): + if not obj.Active: + continue + if hasattr(obj, "Base") and hasattr(obj.Base, "Active"): + if not obj.Base.Active: + continue + + # do the pre_op + if values["OUTPUT_BCNC"]: + comment = PostUtilsParse.create_comment( + "Block-name: " + obj.Label, values["COMMENT_SYMBOL"] + ) + gcode += PostUtilsParse.linenumber(values) + comment + "\n" + comment = PostUtilsParse.create_comment( + "Block-expand: 0", values["COMMENT_SYMBOL"] + ) + gcode += PostUtilsParse.linenumber(values) + comment + "\n" + comment = PostUtilsParse.create_comment( + "Block-enable: 1", values["COMMENT_SYMBOL"] + ) + gcode += PostUtilsParse.linenumber(values) + comment + "\n" + if values["OUTPUT_COMMENTS"]: + if values["SHOW_OPERATION_LABELS"]: + comment = PostUtilsParse.create_comment( + "Begin operation: %s" % obj.Label, values["COMMENT_SYMBOL"] + ) + else: + comment = PostUtilsParse.create_comment( + "Begin operation", values["COMMENT_SYMBOL"] + ) + gcode += PostUtilsParse.linenumber(values) + comment + "\n" + if values["SHOW_MACHINE_UNITS"]: + comment = PostUtilsParse.create_comment( + "Machine units: %s" % values["UNIT_SPEED_FORMAT"], + values["COMMENT_SYMBOL"], + ) + gcode += PostUtilsParse.linenumber(values) + comment + "\n" + if values["OUTPUT_MACHINE_NAME"]: + comment = PostUtilsParse.create_comment( + "Machine: %s, %s" + % (values["MACHINE_NAME"], values["UNIT_SPEED_FORMAT"]), + values["COMMENT_SYMBOL"], + ) + gcode += PostUtilsParse.linenumber(values) + comment + "\n" + for line in values["PRE_OPERATION"].splitlines(False): + gcode += PostUtilsParse.linenumber(values) + line + "\n" + + # get coolant mode + coolantMode = "None" + if ( + hasattr(obj, "CoolantMode") + or hasattr(obj, "Base") + and hasattr(obj.Base, "CoolantMode") + ): + if hasattr(obj, "CoolantMode"): + coolantMode = obj.CoolantMode + else: + coolantMode = obj.Base.CoolantMode + + # turn coolant on if required + if values["ENABLE_COOLANT"]: + if values["OUTPUT_COMMENTS"]: + if not coolantMode == "None": + comment = PostUtilsParse.create_comment( + "Coolant On:" + coolantMode, values["COMMENT_SYMBOL"] + ) + gcode += PostUtilsParse.linenumber(values) + comment + "\n" + if coolantMode == "Flood": + gcode += PostUtilsParse.linenumber(values) + "M8" + "\n" + if coolantMode == "Mist": + gcode += PostUtilsParse.linenumber(values) + "M7" + "\n" + + # process the operation gcode + gcode += PostUtilsParse.parse(values, obj) + + # do the post_op + if values["OUTPUT_COMMENTS"]: + comment = PostUtilsParse.create_comment( + "%s operation: %s" % (values["FINISH_LABEL"], obj.Label), + values["COMMENT_SYMBOL"], + ) + gcode += PostUtilsParse.linenumber(values) + comment + "\n" + for line in values["POST_OPERATION"].splitlines(False): + gcode += PostUtilsParse.linenumber(values) + line + "\n" + + # turn coolant off if required + if values["ENABLE_COOLANT"]: + if not coolantMode == "None": + if values["OUTPUT_COMMENTS"]: + comment = PostUtilsParse.create_comment( + "Coolant Off:" + coolantMode, values["COMMENT_SYMBOL"] + ) + gcode += PostUtilsParse.linenumber(values) + comment + "\n" + gcode += PostUtilsParse.linenumber(values) + "M9" + "\n" + + if values["RETURN_TO"]: + gcode += PostUtilsParse.linenumber(values) + "G0 X%s Y%s Z%s\n" % tuple( + values["RETURN_TO"] + ) + + # do the post_amble + if values["OUTPUT_BCNC"]: + comment = PostUtilsParse.create_comment( + "Block-name: post_amble", values["COMMENT_SYMBOL"] + ) + gcode += PostUtilsParse.linenumber(values) + comment + "\n" + comment = PostUtilsParse.create_comment( + "Block-expand: 0", values["COMMENT_SYMBOL"] + ) + gcode += PostUtilsParse.linenumber(values) + comment + "\n" + comment = PostUtilsParse.create_comment( + "Block-enable: 1", values["COMMENT_SYMBOL"] + ) + gcode += PostUtilsParse.linenumber(values) + comment + "\n" + if values["OUTPUT_COMMENTS"]: + comment = PostUtilsParse.create_comment( + "Begin postamble", values["COMMENT_SYMBOL"] + ) + gcode += PostUtilsParse.linenumber(values) + comment + "\n" + for line in values["TOOLRETURN"].splitlines(False): + gcode += PostUtilsParse.linenumber(values) + line + "\n" + for line in values["SAFETYBLOCK"].splitlines(False): + gcode += PostUtilsParse.linenumber(values) + line + "\n" + for line in values["POSTAMBLE"].splitlines(False): + gcode += PostUtilsParse.linenumber(values) + line + "\n" + + if FreeCAD.GuiUp and values["SHOW_EDITOR"]: + final = gcode + if len(gcode) > 100000: + print("Skipping editor since output is greater than 100kb") + else: + dia = PostUtils.GCodeEditorDialog() + dia.editor.setText(gcode) + result = dia.exec_() + if result: + final = dia.editor.toPlainText() + else: + final = gcode + + print("done postprocessing.") + + if not filename == "-": + gfile = pythonopen(filename, "w", newline=values["END_OF_LINE_CHARACTERS"]) + gfile.write(final) + gfile.close() + + return final diff --git a/src/Mod/Path/PathScripts/PostUtilsParse.py b/src/Mod/Path/PathScripts/PostUtilsParse.py new file mode 100644 index 0000000000..ea2ed95663 --- /dev/null +++ b/src/Mod/Path/PathScripts/PostUtilsParse.py @@ -0,0 +1,579 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2014 Yorik van Havre * +# * Copyright (c) 2014 sliptonic * +# * Copyright (c) 2015 Dan Falck * +# * Copyright (c) 2018, 2019 Gauthier Briere * +# * Copyright (c) 2019, 2020 Schildkroet * +# * Copyright (c) 2022 Larry Woestman * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * 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. * +# * * +# * FreeCAD 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 Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +import re + +import FreeCAD +from FreeCAD import Units + +import Path + +from PathScripts import PostUtils + + +def create_comment(comment_string, comment_symbol): + """Create a comment from a string using the correct comment symbol.""" + if comment_symbol == "(": + comment_string = "(" + comment_string + ")" + else: + comment_string = comment_symbol + comment_string + return comment_string + + +def drill_translate(values, outstring, cmd, params): + """Translate drill cycles.""" + axis_precision_string = "." + str(values["AXIS_PRECISION"]) + "f" + feed_precision_string = "." + str(values["FEED_PRECISION"]) + "f" + + trBuff = "" + + if values["OUTPUT_COMMENTS"]: # Comment the original command + trBuff += ( + linenumber(values) + + create_comment( + values["COMMAND_SPACE"] + + format_outstring(values, outstring) + + values["COMMAND_SPACE"], + values["COMMENT_SYMBOL"], + ) + + "\n" + ) + + # cycle conversion + # currently only cycles in XY are provided (G17) + # other plains ZX (G18) and YZ (G19) are not dealt with : Z drilling only. + drill_X = Units.Quantity(params["X"], FreeCAD.Units.Length) + drill_Y = Units.Quantity(params["Y"], FreeCAD.Units.Length) + drill_Z = Units.Quantity(params["Z"], FreeCAD.Units.Length) + RETRACT_Z = Units.Quantity(params["R"], FreeCAD.Units.Length) + # R less than Z is error + if RETRACT_Z < drill_Z: + trBuff += ( + linenumber(values) + + create_comment( + "Drill cycle error: R less than Z", values["COMMENT_SYMBOL"] + ) + + "\n" + ) + return trBuff + + if values["MOTION_MODE"] == "G91": # G91 relative movements + drill_X += values["CURRENT_X"] + drill_Y += values["CURRENT_Y"] + drill_Z += values["CURRENT_Z"] + RETRACT_Z += values["CURRENT_Z"] + + if values["DRILL_RETRACT_MODE"] == "G98" and values["CURRENT_Z"] >= RETRACT_Z: + RETRACT_Z = values["CURRENT_Z"] + + # get the other parameters + drill_feedrate = Units.Quantity(params["F"], FreeCAD.Units.Velocity) + if cmd == "G83": + drill_Step = Units.Quantity(params["Q"], FreeCAD.Units.Length) + a_bit = ( + drill_Step * 0.05 + ) # NIST 3.5.16.4 G83 Cycle: "current hole bottom, backed off a bit." + elif cmd == "G82": + drill_DwellTime = params["P"] + + # wrap this block to ensure machine's values["MOTION_MODE"] is restored + # in case of error + try: + if values["MOTION_MODE"] == "G91": + trBuff += ( + linenumber(values) + "G90\n" + ) # force absolute coordinates during cycles + + strG0_RETRACT_Z = ( + "G0 Z" + + format( + float(RETRACT_Z.getValueAs(values["UNIT_FORMAT"])), + axis_precision_string, + ) + + "\n" + ) + strF_Feedrate = ( + " F" + + format( + float(drill_feedrate.getValueAs(values["UNIT_SPEED_FORMAT"])), + feed_precision_string, + ) + + "\n" + ) + # print(strF_Feedrate) + + # preliminary movement(s) + if values["CURRENT_Z"] < RETRACT_Z: + trBuff += linenumber(values) + strG0_RETRACT_Z + trBuff += ( + linenumber(values) + + "G0 X" + + format( + float(drill_X.getValueAs(values["UNIT_FORMAT"])), axis_precision_string + ) + + " Y" + + format( + float(drill_Y.getValueAs(values["UNIT_FORMAT"])), axis_precision_string + ) + + "\n" + ) + if values["CURRENT_Z"] > RETRACT_Z: + # NIST GCODE 3.5.16.1 Preliminary and In-Between Motion says G0 to RETRACT_Z + # Here use G1 since retract height may be below surface ! + trBuff += ( + linenumber(values) + + "G1 Z" + + format( + float(RETRACT_Z.getValueAs(values["UNIT_FORMAT"])), + axis_precision_string, + ) + + strF_Feedrate + ) + last_Stop_Z = RETRACT_Z + + # drill moves + if cmd in ("G81", "G82"): + trBuff += ( + linenumber(values) + + "G1 Z" + + format( + float(drill_Z.getValueAs(values["UNIT_FORMAT"])), + axis_precision_string, + ) + + strF_Feedrate + ) + # pause where applicable + if cmd == "G82": + trBuff += linenumber(values) + "G4 P" + str(drill_DwellTime) + "\n" + trBuff += linenumber(values) + strG0_RETRACT_Z + else: # 'G83' + if params["Q"] != 0: + while 1: + if last_Stop_Z != RETRACT_Z: + clearance_depth = ( + last_Stop_Z + a_bit + ) # rapid move to just short of last drilling depth + trBuff += ( + linenumber(values) + + "G0 Z" + + format( + float( + clearance_depth.getValueAs(values["UNIT_FORMAT"]) + ), + axis_precision_string, + ) + + "\n" + ) + next_Stop_Z = last_Stop_Z - drill_Step + if next_Stop_Z > drill_Z: + trBuff += ( + linenumber(values) + + "G1 Z" + + format( + float(next_Stop_Z.getValueAs(values["UNIT_FORMAT"])), + axis_precision_string, + ) + + strF_Feedrate + ) + trBuff += linenumber(values) + strG0_RETRACT_Z + last_Stop_Z = next_Stop_Z + else: + trBuff += ( + linenumber(values) + + "G1 Z" + + format( + float(drill_Z.getValueAs(values["UNIT_FORMAT"])), + axis_precision_string, + ) + + strF_Feedrate + ) + trBuff += linenumber(values) + strG0_RETRACT_Z + break + + except Exception: + pass + + if values["MOTION_MODE"] == "G91": + trBuff += linenumber(values) + "G91\n" # Restore if changed + + return trBuff + + +def dump(obj): + """For debug...""" + for attr in dir(obj): + print("obj.%s = %s" % (attr, getattr(obj, attr))) + + +def format_outstring(values, strTable): + """Construct the line for the final output.""" + s = "" + for w in strTable: + s += w + values["COMMAND_SPACE"] + s = s.strip() + return s + + +def linenumber(values, space=None): + """Output the next line number if appropriate.""" + if values["OUTPUT_LINE_NUMBERS"]: + if space is None: + space = values["COMMAND_SPACE"] + line_num = str(values["line_number"]) + values["line_number"] += values["LINE_INCREMENT"] + return "N" + line_num + space + return "" + + +def parse(values, pathobj): + """Parse a Path.""" + out = "" + lastcommand = None + axis_precision_string = "." + str(values["AXIS_PRECISION"]) + "f" + feed_precision_string = "." + str(values["FEED_PRECISION"]) + "f" + + currLocation = {} # keep track for no doubles + firstmove = Path.Command("G0", {"X": -1, "Y": -1, "Z": -1, "F": 0.0}) + currLocation.update(firstmove.Parameters) # set First location Parameters + + if hasattr(pathobj, "Group"): # We have a compound or project. + if values["OUTPUT_COMMENTS"]: + comment = create_comment( + "Compound: " + pathobj.Label, values["COMMENT_SYMBOL"] + ) + out += linenumber(values) + comment + "\n" + for p in pathobj.Group: + out += parse(values, p) + return out + else: # parsing simple path + + # groups might contain non-path things like stock. + if not hasattr(pathobj, "Path"): + return out + + if values["OUTPUT_PATH_LABELS"] and values["OUTPUT_COMMENTS"]: + comment = create_comment("Path: " + pathobj.Label, values["COMMENT_SYMBOL"]) + out += linenumber(values) + comment + "\n" + + if values["OUTPUT_ADAPTIVE"]: + adaptiveOp = False + opHorizRapid = 0 + opVertRapid = 0 + if "Adaptive" in pathobj.Name: + adaptiveOp = True + if hasattr(pathobj, "ToolController"): + if ( + hasattr(pathobj.ToolController, "HorizRapid") + and pathobj.ToolController.HorizRapid > 0 + ): + opHorizRapid = Units.Quantity( + pathobj.ToolController.HorizRapid, FreeCAD.Units.Velocity + ) + else: + FreeCAD.Console.PrintWarning( + "Tool Controller Horizontal Rapid Values are unset" + "\n" + ) + if ( + hasattr(pathobj.ToolController, "VertRapid") + and pathobj.ToolController.VertRapid > 0 + ): + opVertRapid = Units.Quantity( + pathobj.ToolController.VertRapid, FreeCAD.Units.Velocity + ) + else: + FreeCAD.Console.PrintWarning( + "Tool Controller Vertical Rapid Values are unset" + "\n" + ) + + for c in pathobj.Path.Commands: + + # List of elements in the command, code, and params. + outstring = [] + # command M or G code or comment string + command = c.Name + if command[0] == "(": + if values["OUTPUT_COMMENTS"]: + if values["COMMENT_SYMBOL"] != "(": + command = PostUtils.fcoms(command, values["COMMENT_SYMBOL"]) + else: + continue + if values["OUTPUT_ADAPTIVE"]: + if adaptiveOp and command in values["RAPID_MOVES"]: + if opHorizRapid and opVertRapid: + command = "G1" + else: + outstring.append( + "(Tool Controller Rapid Values are unset)" + "\n" + ) + + outstring.append(command) + + # if modal: suppress the command if it is the same as the last one + if values["MODAL"]: + if command == lastcommand: + outstring.pop(0) + + # Now add the remaining parameters in order + for param in values["PARAMETER_ORDER"]: + if param in c.Parameters: + if param == "F" and ( + currLocation[param] != c.Parameters[param] + or values["OUTPUT_DOUBLES"] + ): + # centroid and linuxcnc don't use rapid speeds + if command not in values["RAPID_MOVES"]: + speed = Units.Quantity( + c.Parameters["F"], FreeCAD.Units.Velocity + ) + if speed.getValueAs(values["UNIT_SPEED_FORMAT"]) > 0.0: + outstring.append( + param + + format( + float( + speed.getValueAs( + values["UNIT_SPEED_FORMAT"] + ) + ), + feed_precision_string, + ) + ) + else: + continue + elif param in ["H", "L", "T"]: + outstring.append(param + str(int(c.Parameters[param]))) + elif param == "D": + if command in ["G41", "G42"]: + outstring.append(param + str(int(c.Parameters[param]))) + elif command in ["G96", "G97"]: + outstring.append( + param + + PostUtils.fmt( + c.Parameters[param], + values["SPINDLE_DECIMALS"], + "G21", + ) + ) + else: # anything else that is supported (G41.1?, G42.1?) + outstring.append(param + str(float(c.Parameters[param]))) + elif param == "P": + if command in ["G2", "G02", "G3", "G03", "G5.2", "G5.3", "G10"]: + outstring.append(param + str(int(c.Parameters[param]))) + elif command in [ + "G4", + "G04", + "G64", + "G76", + "G82", + "G86", + "G89", + ]: + outstring.append(param + str(float(c.Parameters[param]))) + elif command in ["G5", "G05"]: + pos = Units.Quantity( + c.Parameters[param], FreeCAD.Units.Length + ) + outstring.append( + param + + format( + float(pos.getValueAs(values["UNIT_FORMAT"])), + axis_precision_string, + ) + ) + else: # anything else that is supported + outstring.append(param + str(c.Parameters[param])) + elif param == "S": + outstring.append( + param + + PostUtils.fmt( + c.Parameters[param], values["SPINDLE_DECIMALS"], "G21" + ) + ) + else: + if ( + (not values["OUTPUT_DOUBLES"]) + and (param in currLocation) + and (currLocation[param] == c.Parameters[param]) + ): + continue + else: + pos = Units.Quantity( + c.Parameters[param], FreeCAD.Units.Length + ) + outstring.append( + param + + format( + float(pos.getValueAs(values["UNIT_FORMAT"])), + axis_precision_string, + ) + ) + + if values["OUTPUT_ADAPTIVE"]: + if adaptiveOp and command in values["RAPID_MOVES"]: + if opHorizRapid and opVertRapid: + if "Z" not in c.Parameters: + outstring.append( + "F" + + format( + float( + opHorizRapid.getValueAs( + values["UNIT_SPEED_FORMAT"] + ) + ), + axis_precision_string, + ) + ) + else: + outstring.append( + "F" + + format( + float( + opVertRapid.getValueAs( + values["UNIT_SPEED_FORMAT"] + ) + ), + axis_precision_string, + ) + ) + + # store the latest command + lastcommand = command + + currLocation.update(c.Parameters) + # Memorizes the current position for calculating the related movements + # and the withdrawal plan + if command in values["MOTION_COMMANDS"]: + if "X" in c.Parameters: + values["CURRENT_X"] = Units.Quantity( + c.Parameters["X"], FreeCAD.Units.Length + ) + if "Y" in c.Parameters: + values["CURRENT_Y"] = Units.Quantity( + c.Parameters["Y"], FreeCAD.Units.Length + ) + if "Z" in c.Parameters: + values["CURRENT_Z"] = Units.Quantity( + c.Parameters["Z"], FreeCAD.Units.Length + ) + + if command in ("G98", "G99"): + values["DRILL_RETRACT_MODE"] = command + + if command in ("G90", "G91"): + values["MOTION_MODE"] = command + + if values["TRANSLATE_DRILL_CYCLES"]: + if command in values["DRILL_CYCLES_TO_TRANSLATE"]: + out += drill_translate(values, outstring, command, c.Parameters) + # Erase the line we just translated + outstring = [] + + if values["SPINDLE_WAIT"] > 0: + if command in ("M3", "M03", "M4", "M04"): + out += ( + linenumber(values) + format_outstring(values, outstring) + "\n" + ) + out += ( + linenumber(values) + + format_outstring( + values, ["G4", "P%s" % values["SPINDLE_WAIT"]] + ) + + "\n" + ) + outstring = [] + + # Check for Tool Change: + if command in ("M6", "M06"): + if values["OUTPUT_COMMENTS"]: + comment = create_comment( + "Begin toolchange", values["COMMENT_SYMBOL"] + ) + out += linenumber(values) + comment + "\n" + if values["OUTPUT_TOOL_CHANGE"]: + if values["STOP_SPINDLE_FOR_TOOL_CHANGE"]: + # stop the spindle + out += linenumber(values) + "M5\n" + for line in values["TOOL_CHANGE"].splitlines(False): + out += linenumber(values) + line + "\n" + else: + if values["OUTPUT_COMMENTS"]: + # convert the tool change to a comment + comment = create_comment( + values["COMMAND_SPACE"] + + format_outstring(values, outstring) + + values["COMMAND_SPACE"], + values["COMMENT_SYMBOL"], + ) + out += linenumber(values) + comment + "\n" + outstring = [] + + if command == "message" and values["REMOVE_MESSAGES"]: + if values["OUTPUT_COMMENTS"] is False: + out = [] + else: + outstring.pop(0) # remove the command + + if command in values["SUPPRESS_COMMANDS"]: + if values["OUTPUT_COMMENTS"]: + # convert the command to a comment + comment = create_comment( + values["COMMAND_SPACE"] + + format_outstring(values, outstring) + + values["COMMAND_SPACE"], + values["COMMENT_SYMBOL"], + ) + out += linenumber(values) + comment + "\n" + # remove the command + outstring = [] + + # prepend a line number and append a newline + if len(outstring) >= 1: + if values["OUTPUT_LINE_NUMBERS"]: + # In this case we don't want a space after the line number + # because the space is added in the join just below. + outstring.insert(0, (linenumber(values, ""))) + + # append the line to the final output + out += values["COMMAND_SPACE"].join(outstring) + # Note: Do *not* strip `out`, since that forces the allocation + # of a contiguous string & thus quadratic complexity. + out += "\n" + + # add height offset + if command in ("M6", "M06") and values["USE_TLO"]: + out += linenumber(values) + "G43 H" + str(int(c.Parameters["T"])) + "\n" + + # Check for comments containing machine-specific commands + # to pass literally to the controller + if values["ENABLE_MACHINE_SPECIFIC_COMMANDS"]: + m = re.match(r"^\(MC_RUN_COMMAND: ([^)]+)\)$", command) + if m: + raw_command = m.group(1) + out += linenumber(values) + raw_command + "\n" + + return out diff --git a/src/Mod/Path/PathScripts/post/centroid_post.py b/src/Mod/Path/PathScripts/post/centroid_post.py index bd91b9c95d..6fe06202bb 100644 --- a/src/Mod/Path/PathScripts/post/centroid_post.py +++ b/src/Mod/Path/PathScripts/post/centroid_post.py @@ -24,6 +24,7 @@ # *************************************************************************** from __future__ import print_function +import os import FreeCAD from FreeCAD import Units import datetime @@ -81,7 +82,7 @@ COMMENT = ";" # gCode header with information about CAD-software, post-processor # and date/time if FreeCAD.ActiveDocument: - cam_file = FreeCAD.ActiveDocument.FileName + cam_file = os.path.basename(FreeCAD.ActiveDocument.FileName) else: cam_file = "" diff --git a/src/Mod/Path/PathScripts/post/grbl_post.py b/src/Mod/Path/PathScripts/post/grbl_post.py index 94d9252db5..4613683304 100755 --- a/src/Mod/Path/PathScripts/post/grbl_post.py +++ b/src/Mod/Path/PathScripts/post/grbl_post.py @@ -407,6 +407,8 @@ def export(objectslist, filename, argstring): gfile.write(final) gfile.close() + return final + def linenumber(): if not OUTPUT_LINE_NUMBERS: diff --git a/src/Mod/Path/PathScripts/post/refactored_centroid_post.py b/src/Mod/Path/PathScripts/post/refactored_centroid_post.py new file mode 100644 index 0000000000..0aaa107ab6 --- /dev/null +++ b/src/Mod/Path/PathScripts/post/refactored_centroid_post.py @@ -0,0 +1,255 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2015 Dan Falck * +# * Copyright (c) 2020 Schildkroet * +# * Copyright (c) 2022 Larry Woestman * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * 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. * +# * * +# * FreeCAD 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 Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +from __future__ import print_function + +from PathScripts import PostUtilsArguments +from PathScripts import PostUtilsExport + +# +# The following variables need to be global variables +# to keep the PathPostProcessor.load method happy: +# +# TOOLTIP +# TOOLTIP_ARGS +# UNITS +# +# The "argument_defaults", "arguments_visible", and the "values" hashes +# need to be defined before the "init_shared_arguments" routine can be +# called to create TOOLTIP_ARGS, so they also end up having to be globals. +# TOOLTIP_ARGS can be defined, so they end up being global variables also. +# +TOOLTIP = """ +This is a postprocessor file for the Path workbench. It is used to +take a pseudo-gcode fragment outputted by a Path object, and output +real GCode suitable for a centroid 3 axis mill. This postprocessor, once placed +in the appropriate PathScripts folder, can be used directly from inside +FreeCAD, via the GUI importer or via python scripts with: + +import refactored_centroid_post +refactored_centroid_post.export(object,"/path/to/file.ncc","") +""" +# +# Default to metric mode +# +UNITS = "G21" + + +def init_values(values): + """Initialize values that are used throughout the postprocessor.""" + # + global UNITS + + PostUtilsArguments.init_shared_values(values) + # + # Set any values here that need to override the default values set + # in the init_shared_values routine. + # + # Use 4 digits for axis precision by default. + # + values["AXIS_PRECISION"] = 4 + values["DEFAULT_AXIS_PRECISION"] = 4 + values["DEFAULT_INCH_AXIS_PRECISION"] = 4 + # + # Use ";" as the comment symbol + # + values["COMMENT_SYMBOL"] = ";" + # + # Use 1 digit for feed precision by default. + # + values["FEED_PRECISION"] = 1 + values["DEFAULT_FEED_PRECISION"] = 1 + values["DEFAULT_INCH_FEED_PRECISION"] = 1 + # + # This value usually shows up in the post_op comment as "Finish operation:". + # Change it to "End" to produce "End operation:". + # + values["FINISH_LABEL"] = "End" + # + # If this value is True, then a list of tool numbers + # with their labels are output just before the preamble. + # + values["LIST_TOOLS_IN_PREAMBLE"] = True + # + # Used in the argparser code as the "name" of the postprocessor program. + # This would normally show up in the usage message in the TOOLTIP_ARGS, + # but we are suppressing the usage message, so it doesn't show up after all. + # + values["MACHINE_NAME"] = "Centroid" + # + # This list controls the order of parameters in a line during output. + # centroid doesn't want K properties on XY plane; Arcs need work. + # + values["PARAMETER_ORDER"] = [ + "X", + "Y", + "Z", + "A", + "B", + "I", + "J", + "F", + "S", + "T", + "Q", + "R", + "L", + "H", + ] + # + # Any commands in this value will be output as the last commands + # in the G-code file. + # + values["POSTAMBLE"] = """M99""" + values["POSTPROCESSOR_FILE_NAME"] = __name__ + # + # Any commands in this value will be output after the header and + # safety block at the beginning of the G-code file. + # + values["PREAMBLE"] = """G53 G00 G17""" + # + # Output any messages. + # + values["REMOVE_MESSAGES"] = False + # + # Any commands in this value are output after the header but before the preamble, + # then again after the TOOLRETURN but before the POSTAMBLE. + # + values["SAFETYBLOCK"] = """G90 G80 G40 G49""" + # + # Do not show the current machine units just before the PRE_OPERATION. + # + values["SHOW_MACHINE_UNITS"] = False + # + # Do not show the current operation label just before the PRE_OPERATION. + # + values["SHOW_OPERATION_LABELS"] = False + # + # Do not output an M5 command to stop the spindle for tool changes. + # + values["STOP_SPINDLE_FOR_TOOL_CHANGE"] = False + # + # spindle off, height offset canceled, spindle retracted + # (M25 is a centroid command to retract spindle) + # + values[ + "TOOLRETURN" + ] = """M5 +M25 +G49 H0""" + values["UNITS"] = UNITS + # + # Default to not outputting a G43 following tool changes + # + values["USE_TLO"] = False + # + # This was in the original centroid postprocessor file + # but does not appear to be used anywhere. + # + # ZAXISRETURN = """G91 G28 X0 Z0 G90""" + # + + +def init_argument_defaults(argument_defaults): + """Initialize which arguments (in a pair) are shown as the default argument.""" + PostUtilsArguments.init_argument_defaults(argument_defaults) + # + # Modify which argument to show as the default in flag-type arguments here. + # If the value is True, the first argument will be shown as the default. + # If the value is False, the second argument will be shown as the default. + # + # For example, if you want to show Metric mode as the default, use: + # argument_defaults["metric_inch"] = True + # + # If you want to show that "Don't pop up editor for writing output" is + # the default, use: + # argument_defaults["show-editor"] = False. + # + # Note: You also need to modify the corresponding entries in the "values" hash + # to actually make the default value(s) change to match. + # + + +def init_arguments_visible(arguments_visible): + """Initialize which argument pairs are visible in TOOLTIP_ARGS.""" + PostUtilsArguments.init_arguments_visible(arguments_visible) + # + # Modify the visibility of any arguments from the defaults here. + # + arguments_visible["axis-modal"] = False + arguments_visible["precision"] = False + arguments_visible["tlo"] = False + + +def init_arguments(values, argument_defaults, arguments_visible): + """Initialize the shared argument definitions.""" + parser = PostUtilsArguments.init_shared_arguments(values, argument_defaults, arguments_visible) + # + # Add any argument definitions that are not shared with all other postprocessors here. + # + return parser + + +# +# Creating global variables and using functions to modify them +# is useful for being able to test things later. +# +values = {} +init_values(values) +argument_defaults = {} +init_argument_defaults(argument_defaults) +arguments_visible = {} +init_arguments_visible(arguments_visible) +parser = init_arguments(values, argument_defaults, arguments_visible) +# +# The TOOLTIP_ARGS value is created from the help information about the arguments. +# +TOOLTIP_ARGS = parser.format_help() + + +def export(objectslist, filename, argstring): + """Postprocess the objects in objectslist to filename.""" + # + global parser + global UNITS + global values + + # print(parser.format_help()) + + (flag, args) = PostUtilsArguments.process_shared_arguments(values, parser, argstring) + if not flag: + return None + # + # Process any additional arguments here + # + + # + # Update the global variables that might have been modified + # while processing the arguments. + # + UNITS = values["UNITS"] + + return PostUtilsExport.export_common(values, objectslist, filename) diff --git a/src/Mod/Path/PathScripts/post/refactored_grbl_post.py b/src/Mod/Path/PathScripts/post/refactored_grbl_post.py new file mode 100644 index 0000000000..61efea92ac --- /dev/null +++ b/src/Mod/Path/PathScripts/post/refactored_grbl_post.py @@ -0,0 +1,222 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2014 sliptonic * +# * Copyright (c) 2018, 2019 Gauthier Briere * +# * Copyright (c) 2019, 2020 Schildkroet * +# * Copyright (c) 2022 Larry Woestman * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * 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. * +# * * +# * FreeCAD 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 Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +from __future__ import print_function + +from PathScripts import PostUtilsArguments +from PathScripts import PostUtilsExport + +# +# The following variables need to be global variables +# to keep the PathPostProcessor.load method happy: +# +# TOOLTIP +# TOOLTIP_ARGS +# UNITS +# +# The "argument_defaults", "arguments_visible", and the "values" hashes +# need to be defined before the "init_shared_arguments" routine can be +# called to create TOOLTIP_ARGS, so they also end up having to be globals. +# +TOOLTIP = """ +Generate g-code from a Path that is compatible with the grbl controller: + +import refactored_grbl_post +refactored_grbl_post.export(object, "/path/to/file.ncc") +""" +# +# Default to metric mode +# +UNITS = "G21" + + +def init_values(values): + """Initialize values that are used throughout the postprocessor.""" + # + global UNITS + + PostUtilsArguments.init_shared_values(values) + # + # Set any values here that need to override the default values set + # in the init_shared_values routine. + # + # + # If this is set to True, then commands that are placed in + # comments that look like (MC_RUN_COMMAND: blah) will be output. + # + values["ENABLE_MACHINE_SPECIFIC_COMMANDS"] = True + # + # Used in the argparser code as the "name" of the postprocessor program. + # This would normally show up in the usage message in the TOOLTIP_ARGS, + # but we are suppressing the usage message, so it doesn't show up after all. + # + values["MACHINE_NAME"] = "Grbl" + # + # Default to outputting Path labels at the beginning of each Path. + # + values["OUTPUT_PATH_LABELS"] = True + # + # Default to not outputting M6 tool changes (comment it) as grbl currently does not handle it + # + values["OUTPUT_TOOL_CHANGE"] = False + # + # The order of the parameters. + # Arcs may only work on the XY plane (this needs to be verified). + # + values["PARAMETER_ORDER"] = [ + "X", + "Y", + "Z", + "A", + "B", + "C", + "U", + "V", + "W", + "I", + "J", + "K", + "F", + "S", + "T", + "Q", + "R", + "L", + "P", + ] + # + # Any commands in this value will be output as the last commands + # in the G-code file. + # + values[ + "POSTAMBLE" + ] = """M5 +G17 G90 +M2""" + values["POSTPROCESSOR_FILE_NAME"] = __name__ + # + # Any commands in this value will be output after the header and + # safety block at the beginning of the G-code file. + # + values["PREAMBLE"] = """G17 G90""" + # + # Do not show the current machine units just before the PRE_OPERATION. + # + values["SHOW_MACHINE_UNITS"] = False + values["UNITS"] = UNITS + # + # Default to not outputting a G43 following tool changes + # + values["USE_TLO"] = False + + +def init_argument_defaults(argument_defaults): + """Initialize which arguments (in a pair) are shown as the default argument.""" + PostUtilsArguments.init_argument_defaults(argument_defaults) + # + # Modify which argument to show as the default in flag-type arguments here. + # If the value is True, the first argument will be shown as the default. + # If the value is False, the second argument will be shown as the default. + # + # For example, if you want to show Metric mode as the default, use: + # argument_defaults["metric_inch"] = True + # + # If you want to show that "Don't pop up editor for writing output" is + # the default, use: + # argument_defaults["show-editor"] = False. + # + # Note: You also need to modify the corresponding entries in the "values" hash + # to actually make the default value(s) change to match. + # + argument_defaults["tlo"] = False + argument_defaults["tool_change"] = False + + +def init_arguments_visible(arguments_visible): + """Initialize which argument pairs are visible in TOOLTIP_ARGS.""" + PostUtilsArguments.init_arguments_visible(arguments_visible) + # + # Modify the visibility of any arguments from the defaults here. + # + arguments_visible["bcnc"] = True + arguments_visible["axis-modal"] = False + arguments_visible["return-to"] = True + arguments_visible["tlo"] = False + arguments_visible["tool_change"] = True + arguments_visible["translate_drill"] = True + arguments_visible["wait-for-spindle"] = True + + +def init_arguments(values, argument_defaults, arguments_visible): + """Initialize the shared argument definitions.""" + parser = PostUtilsArguments.init_shared_arguments(values, argument_defaults, arguments_visible) + # + # Add any argument definitions that are not shared with all other postprocessors here. + # + return parser + + +# +# Creating global variables and using functions to modify them +# is useful for being able to test things later. +# +values = {} +init_values(values) +argument_defaults = {} +init_argument_defaults(argument_defaults) +arguments_visible = {} +init_arguments_visible(arguments_visible) +parser = init_arguments(values, argument_defaults, arguments_visible) +# +# The TOOLTIP_ARGS value is created from the help information about the arguments. +# +TOOLTIP_ARGS = parser.format_help() + + +def export(objectslist, filename, argstring): + """Postprocess the objects in objectslist to filename.""" + # + global parser + global UNITS + global values + + # print(parser.format_help()) + + (flag, args) = PostUtilsArguments.process_shared_arguments(values, parser, argstring) + if not flag: + return None + # + # Process any additional arguments here + # + + # + # Update the global variables that might have been modified + # while processing the arguments. + # + UNITS = values["UNITS"] + + return PostUtilsExport.export_common(values, objectslist, filename) diff --git a/src/Mod/Path/PathScripts/post/refactored_linuxcnc_post.py b/src/Mod/Path/PathScripts/post/refactored_linuxcnc_post.py new file mode 100644 index 0000000000..87bc34f6bc --- /dev/null +++ b/src/Mod/Path/PathScripts/post/refactored_linuxcnc_post.py @@ -0,0 +1,189 @@ +# *************************************************************************** +# * Copyright (c) 2014 sliptonic * +# * Copyright (c) 2022 Larry Woestman * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * 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. * +# * * +# * FreeCAD 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 Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +from __future__ import print_function + +from PathScripts import PostUtilsArguments +from PathScripts import PostUtilsExport + +# +# The following variables need to be global variables +# to keep the PathPostProcessor.load method happy: +# +# TOOLTIP +# TOOLTIP_ARGS +# UNITS +# +# The "argument_defaults", "arguments_visible", and the "values" hashes +# need to be defined before the "init_shared_arguments" routine can be +# called to create TOOLTIP_ARGS, so they also end up having to be globals. +# +TOOLTIP = """This is a postprocessor file for the Path workbench. It is used to +take a pseudo-gcode fragment outputted by a Path object, and output +real GCode suitable for a linuxcnc 3 axis mill. This postprocessor, once placed +in the appropriate PathScripts folder, can be used directly from inside +FreeCAD, via the GUI importer or via python scripts with: + +import refactored_linuxcnc_post +refactored_linuxcnc_post.export(object,"/path/to/file.ncc","") +""" +# +# Default to metric mode +# +UNITS = "G21" + + +def init_values(values): + """Initialize values that are used throughout the postprocessor.""" + # + global UNITS + + PostUtilsArguments.init_shared_values(values) + # + # Set any values here that need to override the default values set + # in the init_shared_values routine. + # + values["ENABLE_COOLANT"] = True + # the order of parameters + # linuxcnc doesn't want K properties on XY plane; Arcs need work. + values["PARAMETER_ORDER"] = [ + "X", + "Y", + "Z", + "A", + "B", + "C", + "I", + "J", + "F", + "S", + "T", + "Q", + "R", + "L", + "H", + "D", + "P", + ] + # + # Used in the argparser code as the "name" of the postprocessor program. + # This would normally show up in the usage message in the TOOLTIP_ARGS, + # but we are suppressing the usage message, so it doesn't show up after all. + # + values["MACHINE_NAME"] = "LinuxCNC" + # + # Any commands in this value will be output as the last commands + # in the G-code file. + # + values[ + "POSTAMBLE" + ] = """M05 +G17 G54 G90 G80 G40 +M2""" + values["POSTPROCESSOR_FILE_NAME"] = __name__ + # + # Any commands in this value will be output after the header and + # safety block at the beginning of the G-code file. + # + values["PREAMBLE"] = """G17 G54 G40 G49 G80 G90""" + values["UNITS"] = UNITS + + +def init_argument_defaults(argument_defaults): + """Initialize which arguments (in a pair) are shown as the default argument.""" + PostUtilsArguments.init_argument_defaults(argument_defaults) + # + # Modify which argument to show as the default in flag-type arguments here. + # If the value is True, the first argument will be shown as the default. + # If the value is False, the second argument will be shown as the default. + # + # For example, if you want to show Metric mode as the default, use: + # argument_defaults["metric_inch"] = True + # + # If you want to show that "Don't pop up editor for writing output" is + # the default, use: + # argument_defaults["show-editor"] = False. + # + # Note: You also need to modify the corresponding entries in the "values" hash + # to actually make the default value(s) change to match. + # + + +def init_arguments_visible(arguments_visible): + """Initialize which argument pairs are visible in TOOLTIP_ARGS.""" + PostUtilsArguments.init_arguments_visible(arguments_visible) + # + # Modify the visibility of any arguments from the defaults here. + # + + +def init_arguments(values, argument_defaults, arguments_visible): + """Initialize the shared argument definitions.""" + parser = PostUtilsArguments.init_shared_arguments(values, argument_defaults, arguments_visible) + # + # Add any argument definitions that are not shared with all other postprocessors here. + # + return parser + + +# +# Creating global variables and using functions to modify them +# is useful for being able to test things later. +# +values = {} +init_values(values) +argument_defaults = {} +init_argument_defaults(argument_defaults) +arguments_visible = {} +init_arguments_visible(arguments_visible) +parser = init_arguments(values, argument_defaults, arguments_visible) +# +# The TOOLTIP_ARGS value is created from the help information about the arguments. +# +TOOLTIP_ARGS = parser.format_help() + + +def export(objectslist, filename, argstring): + """Postprocess the objects in objectslist to filename.""" + # + global parser + global UNITS + global values + + # print(parser.format_help()) + + (flag, args) = PostUtilsArguments.process_shared_arguments(values, parser, argstring) + if not flag: + return None + # + # Process any additional arguments here + # + + # + # Update the global variables that might have been modified + # while processing the arguments. + # + UNITS = values["UNITS"] + + return PostUtilsExport.export_common(values, objectslist, filename) diff --git a/src/Mod/Path/PathScripts/post/refactored_mach3_mach4_post.py b/src/Mod/Path/PathScripts/post/refactored_mach3_mach4_post.py new file mode 100644 index 0000000000..17d0e40f60 --- /dev/null +++ b/src/Mod/Path/PathScripts/post/refactored_mach3_mach4_post.py @@ -0,0 +1,196 @@ +# *************************************************************************** +# * Copyright (c) 2014 sliptonic * +# * Copyright (c) 2022 Larry Woestman * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * 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. * +# * * +# * FreeCAD 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 Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# ***************************************************************************/ +from __future__ import print_function + +from PathScripts import PostUtilsArguments +from PathScripts import PostUtilsExport + +# +# The following variables need to be global variables +# to keep the PathPostProcessor.load method happy: +# +# TOOLTIP +# TOOLTIP_ARGS +# UNITS +# +# The "argument_defaults", "arguments_visible", and the "values" hashes +# need to be defined before the "init_shared_arguments" routine can be +# called to create TOOLTIP_ARGS, so they also end up having to be globals. +# +TOOLTIP = """ +This is a postprocessor file for the Path workbench. It is used to +take a pseudo-gcode fragment outputted by a Path object, and output +real GCode suitable for a mach3_4 3 axis mill. This postprocessor, once placed +in the appropriate PathScripts folder, can be used directly from inside +FreeCAD, via the GUI importer or via python scripts with: + +import mach3_mach4_post +mach3_mach4_post.export(object,"/path/to/file.ncc","") +""" +# +# Default to metric mode +# +UNITS = "G21" + + +def init_values(values): + """Initialize values that are used throughout the postprocessor.""" + # + global UNITS + + PostUtilsArguments.init_shared_values(values) + # + # Set any values here that need to override the default values set + # in the init_shared_values routine. + # + values["ENABLE_COOLANT"] = True + # + # Used in the argparser code as the "name" of the postprocessor program. + # This would normally show up in the usage message in the TOOLTIP_ARGS, + # but we are suppressing the usage message, so it doesn't show up after all. + # + values["MACHINE_NAME"] = "mach3_4" + # Enable special processing for operations with "Adaptive" in the name + values["OUTPUT_ADAPTIVE"] = True + # Output the machine name for mach3_mach4 instead of the machine units alone. + values["OUTPUT_MACHINE_NAME"] = True + # the order of parameters + # mach3_mach4 doesn't want K properties on XY plane; Arcs need work. + values["PARAMETER_ORDER"] = [ + "X", + "Y", + "Z", + "A", + "B", + "C", + "I", + "J", + "F", + "S", + "T", + "Q", + "R", + "L", + "H", + "D", + "P", + ] + # + # Any commands in this value will be output as the last commands + # in the G-code file. + # + values[ + "POSTAMBLE" + ] = """M05 +G17 G54 G90 G80 G40 +M2""" + values["POSTPROCESSOR_FILE_NAME"] = __name__ + # + # Any commands in this value will be output after the header and + # safety block at the beginning of the G-code file. + # + values["PREAMBLE"] = """G17 G54 G40 G49 G80 G90""" + # Output the machine name for mach3_mach4 instead of the machine units alone. + values["SHOW_MACHINE_UNITS"] = False + values["UNITS"] = UNITS + + +def init_argument_defaults(argument_defaults): + """Initialize which arguments (in a pair) are shown as the default argument.""" + PostUtilsArguments.init_argument_defaults(argument_defaults) + # + # Modify which argument to show as the default in flag-type arguments here. + # If the value is True, the first argument will be shown as the default. + # If the value is False, the second argument will be shown as the default. + # + # For example, if you want to show Metric mode as the default, use: + # argument_defaults["metric_inch"] = True + # + # If you want to show that "Don't pop up editor for writing output" is + # the default, use: + # argument_defaults["show-editor"] = False. + # + # Note: You also need to modify the corresponding entries in the "values" hash + # to actually make the default value(s) change to match. + # + + +def init_arguments_visible(arguments_visible): + """Initialize which argument pairs are visible in TOOLTIP_ARGS.""" + PostUtilsArguments.init_arguments_visible(arguments_visible) + # + # Modify the visibility of any arguments from the defaults here. + # + arguments_visible["axis-modal"] = True + + +def init_arguments(values, argument_defaults, arguments_visible): + """Initialize the shared argument definitions.""" + parser = PostUtilsArguments.init_shared_arguments(values, argument_defaults, arguments_visible) + # + # Add any argument definitions that are not shared with all other postprocessors here. + # + return parser + + +# +# Creating global variables and using functions to modify them +# is useful for being able to test things later. +# +values = {} +init_values(values) +argument_defaults = {} +init_argument_defaults(argument_defaults) +arguments_visible = {} +init_arguments_visible(arguments_visible) +parser = init_arguments(values, argument_defaults, arguments_visible) +# +# The TOOLTIP_ARGS value is created from the help information about the arguments. +# +TOOLTIP_ARGS = parser.format_help() + + +def export(objectslist, filename, argstring): + """Postprocess the objects in objectslist to filename.""" + # + global parser + global UNITS + global values + + # print(parser.format_help()) + + (flag, args) = PostUtilsArguments.process_shared_arguments(values, parser, argstring) + if not flag: + return None + # + # Process any additional arguments here + # + + # + # Update the global variables that might have been modified + # while processing the arguments. + # + UNITS = values["UNITS"] + + return PostUtilsExport.export_common(values, objectslist, filename) diff --git a/src/Mod/Path/PathScripts/post/refactored_test_post.py b/src/Mod/Path/PathScripts/post/refactored_test_post.py new file mode 100644 index 0000000000..0c8dce879c --- /dev/null +++ b/src/Mod/Path/PathScripts/post/refactored_test_post.py @@ -0,0 +1,216 @@ +# *************************************************************************** +# * Copyright (c) 2014 sliptonic * +# * Copyright (c) 2022 Larry Woestman * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * 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. * +# * * +# * FreeCAD 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 Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +from __future__ import print_function + +from PathScripts import PostUtilsArguments +from PathScripts import PostUtilsExport + +# +# The following variables need to be global variables +# to keep the PathPostProcessor.load method happy: +# +# TOOLTIP +# TOOLTIP_ARGS +# UNITS +# +# The "argument_defaults", "arguments_visible", and the "values" hashes +# need to be defined before the "init_shared_arguments" routine can be +# called to create TOOLTIP_ARGS, so they also end up having to be globals. +# +TOOLTIP = """This is a postprocessor file for the Path workbench. It is used to +test the postprocessor code. It probably isn't useful for "real" gcode. + +import refactored_test_post +refactored_test_post.export(object,"/path/to/file.ncc","") +""" +# +# Default to metric mode +# +UNITS = "G21" + + +def init_values(values): + """Initialize values that are used throughout the postprocessor.""" + # + global UNITS + + PostUtilsArguments.init_shared_values(values) + # + # Set any values here that need to override the default values set + # in the init_shared_values routine. + # + # Turn off as much functionality as possible by default. + # Then the tests can turn back on the appropriate options as needed. + # + # Used in the argparser code as the "name" of the postprocessor program. + # This would normally show up in the usage message in the TOOLTIP_ARGS, + # but we are suppressing the usage message, so it doesn't show up after all. + # + values["MACHINE_NAME"] = "test" + # + # Don't output comments by default + # + values["OUTPUT_COMMENTS"] = False + # + # Don't output the header by default + # + values["OUTPUT_HEADER"] = False + # + # Convert M56 tool change commands to comments, + # which are then suppressed by default. + # + values["OUTPUT_TOOL_CHANGE"] = False + # + # Enable as many parameters as possible to be output by default + # + values["PARAMETER_ORDER"] = [ + "X", + "Y", + "Z", + "A", + "B", + "C", + "U", + "V", + "W", + "I", + "J", + "K", + "F", + "S", + "T", + "Q", + "R", + "L", + "H", + "D", + "P", + ] + values["POSTPROCESSOR_FILE_NAME"] = __name__ + # + # Do not show the editor by default since we are testing. + # + values["SHOW_EDITOR"] = False + # + # Don't show the current machine units by default + # + values["SHOW_MACHINE_UNITS"] = False + # + # Don't show the current operation label by default. + # + values["SHOW_OPERATION_LABELS"] = False + # + # Don't output an M5 command to stop the spindle after an M6 tool change by default. + # + values["STOP_SPINDLE_FOR_TOOL_CHANGE"] = False + # + # Don't output a G43 tool length command following tool changes by default. + # + values["USE_TLO"] = False + values["UNITS"] = UNITS + + +def init_argument_defaults(argument_defaults): + """Initialize which arguments (in a pair) are shown as the default argument.""" + PostUtilsArguments.init_argument_defaults(argument_defaults) + # + # Modify which argument to show as the default in flag-type arguments here. + # If the value is True, the first argument will be shown as the default. + # If the value is False, the second argument will be shown as the default. + # + # For example, if you want to show Metric mode as the default, use: + # argument_defaults["metric_inch"] = True + # + # If you want to show that "Don't pop up editor for writing output" is + # the default, use: + # argument_defaults["show-editor"] = False. + # + # Note: You also need to modify the corresponding entries in the "values" hash + # to actually make the default value(s) change to match. + # + + +def init_arguments_visible(arguments_visible): + """Initialize which argument pairs are visible in TOOLTIP_ARGS.""" + PostUtilsArguments.init_arguments_visible(arguments_visible) + # + # Modify the visibility of any arguments from the defaults here. + # + # + # Make all arguments invisible by default. + # + for k in iter(arguments_visible): + arguments_visible[k] = False + + +def init_arguments(values, argument_defaults, arguments_visible): + """Initialize the shared argument definitions.""" + parser = PostUtilsArguments.init_shared_arguments(values, argument_defaults, arguments_visible) + # + # Add any argument definitions that are not shared with all other postprocessors here. + # + return parser + + +# +# Creating global variables and using functions to modify them +# is useful for being able to test things later. +# +values = {} +init_values(values) +argument_defaults = {} +init_argument_defaults(argument_defaults) +arguments_visible = {} +init_arguments_visible(arguments_visible) +parser = init_arguments(values, argument_defaults, arguments_visible) +# +# The TOOLTIP_ARGS value is created from the help information about the arguments. +# +TOOLTIP_ARGS = parser.format_help() + + +def export(objectslist, filename, argstring): + """Postprocess the objects in objectslist to filename.""" + # + global parser + global UNITS + global values + + # print(parser.format_help()) + + (flag, args) = PostUtilsArguments.process_shared_arguments(values, parser, argstring) + if not flag: + return None + # + # Process any additional arguments here + # + + # + # Update the global variables that might have been modified + # while processing the arguments. + # + UNITS = values["UNITS"] + + return PostUtilsExport.export_common(values, objectslist, filename) diff --git a/src/Mod/Path/PathTests/TestCentroidPost.py b/src/Mod/Path/PathTests/TestCentroidPost.py new file mode 100644 index 0000000000..c77f133c88 --- /dev/null +++ b/src/Mod/Path/PathTests/TestCentroidPost.py @@ -0,0 +1,308 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2022 sliptonic * +# * Copyright (c) 2022 Larry Woestman * +# * * +# * 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 * +# * * +# *************************************************************************** + +from importlib import reload + +import FreeCAD + +# import Part +import Path +import PathScripts.PathLog as PathLog +import PathTests.PathTestUtils as PathTestUtils +from PathScripts.post import centroid_post as postprocessor + + +PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +PathLog.trackModule(PathLog.thisModule()) + + +class TestCentroidPost(PathTestUtils.PathTestBase): + @classmethod + def setUpClass(cls): + """setUpClass()... + This method is called upon instantiation of this test class. Add code + and objects here that are needed for the duration of the test() methods + in this class. In other words, set up the 'global' test environment + here; use the `setUp()` method to set up a 'local' test environment. + This method does not have access to the class `self` reference, but it + is able to call static methods within this same class. + """ + + # Open existing FreeCAD document with test geometry + FreeCAD.newDocument("Unnamed") + + @classmethod + def tearDownClass(cls): + """tearDownClass()... + This method is called prior to destruction of this test class. Add + code and objects here that cleanup the test environment after the + test() methods in this class have been executed. This method does + not have access to the class `self` reference. This method + is able to call static methods within this same class. + """ + # Close geometry document without saving + FreeCAD.closeDocument(FreeCAD.ActiveDocument.Name) + + # Setup and tear down methods called before and after each unit test + def setUp(self): + """setUp()... + This method is called prior to each `test()` method. Add code and + objects here that are needed for multiple `test()` methods. + """ + self.doc = FreeCAD.ActiveDocument + self.con = FreeCAD.Console + self.docobj = FreeCAD.ActiveDocument.addObject("Path::Feature", "testpath") + reload( + postprocessor + ) # technical debt. This shouldn't be necessary but here to bypass a bug + + def tearDown(self): + """tearDown()... + This method is called after each test() method. Add cleanup instructions here. + Such cleanup instructions will likely undo those in the setUp() method. + """ + FreeCAD.ActiveDocument.removeObject("testpath") + + def test000(self): + """Test Output Generation. + Empty path. Produces only the preamble and postable. + """ + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + # Test generating with header + # Header contains a time stamp that messes up unit testing. + # Only test length of result. + args = "--no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertTrue(len(gcode.splitlines()) == 16) + + # Test without header + expected = """G90 G80 G40 G49 +;begin preamble +G53 G00 G17 +G21 +;begin operation +;end operation: testpath +;begin postamble +M5 +M25 +G49 H0 +G90 G80 G40 G49 +M99 +""" + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode, expected) + + # test without comments + expected = """G90 G80 G40 G49 +G53 G00 G17 +G21 +M5 +M25 +G49 H0 +G90 G80 G40 G49 +M99 +""" + + args = "--no-header --no-comments --no-show-editor" + # args = ("--no-header --no-comments --no-show-editor --precision=2") + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode, expected) + + def test010(self): + """Test command Generation. + Test Precision + """ + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "G0 X10.0000 Y20.0000 Z30.0000" + self.assertEqual(result, expected) + + args = "--no-header --axis-precision=2 --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "G0 X10.00 Y20.00 Z30.00" + self.assertEqual(result, expected) + + def test020(self): + """ + Test Line Numbers + """ + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --line-numbers --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "N150 G0 X10.0000 Y20.0000 Z30.0000" + self.assertEqual(result, expected) + + def test030(self): + """ + Test Pre-amble + """ + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + # + # The original centroid postprocessor does not have a + # --preamble option. We end up with the default preamble. + # + args = "--no-header --no-comments --preamble='G18 G55' --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[1] + self.assertEqual(result, "G53 G00 G17") + + def test040(self): + """ + Test Post-amble + """ + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + # + # The original centroid postprocessor does not have a + # --postamble option. We end up with the default postamble. + # + args = "--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[-1], "M99") + + def test050(self): + """ + Test inches + """ + + c = Path.Command("G0 X10 Y20 Z30") + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --inches --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[3], "G20") + + result = gcode.splitlines()[5] + expected = "G0 X0.3937 Y0.7874 Z1.1811" + self.assertEqual(result, expected) + + args = "--no-header --inches --axis-precision=2 --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "G0 X0.39 Y0.79 Z1.18" + self.assertEqual(result, expected) + + def test060(self): + """ + Test test modal + Suppress the command name if the same as previous + """ + c = Path.Command("G0 X10 Y20 Z30") + c1 = Path.Command("G0 X10 Y30 Z30") + + self.docobj.Path = Path.Path([c, c1]) + postables = [self.docobj] + + # + # The original centroid postprocessor does not have a + # --modal option. We end up with the original gcode. + # + args = "--no-header --modal --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[6] + expected = "G0 X10.0000 Y30.0000 Z30.0000" + self.assertEqual(result, expected) + + def test070(self): + """ + Test axis modal + Suppress the axis coordinate if the same as previous + """ + c = Path.Command("G0 X10 Y20 Z30") + c1 = Path.Command("G0 X10 Y30 Z30") + + self.docobj.Path = Path.Path([c, c1]) + postables = [self.docobj] + + # + # The original centroid postprocessor does not have an + # --axis-modal option. We end up with the original gcode. + # + args = "--no-header --axis-modal --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[6] + expected = "G0 X10.0000 Y30.0000 Z30.0000" + self.assertEqual(result, expected) + + def test080(self): + """ + Test tool change + """ + c = Path.Command("M6 T2") + c2 = Path.Command("M3 S3000") + self.docobj.Path = Path.Path([c, c2]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[5], "M6 T2") + self.assertEqual(gcode.splitlines()[6], "M3 S3000") + + # suppress TLO + # + # The original centroid postprocessor does not have an + # --no-tlo option. We end up with the original gcode. + # + args = "--no-header --no-tlo --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[6], "M3 S3000") + + def test090(self): + """ + Test comment + """ + + c = Path.Command("(comment)") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = ";comment" + self.assertEqual(result, expected) diff --git a/src/Mod/Path/PathTests/TestGrblPost.py b/src/Mod/Path/PathTests/TestGrblPost.py new file mode 100644 index 0000000000..24ff17a9af --- /dev/null +++ b/src/Mod/Path/PathTests/TestGrblPost.py @@ -0,0 +1,297 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2022 sliptonic * +# * * +# * 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 Part +import Path +import PathScripts.PathLog as PathLog +import PathTests.PathTestUtils as PathTestUtils +from importlib import reload +from PathScripts.post import grbl_post as postprocessor + + +PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +PathLog.trackModule(PathLog.thisModule()) + + +class TestGrblPost(PathTestUtils.PathTestBase): + @classmethod + def setUpClass(cls): + """setUpClass()... + This method is called upon instantiation of this test class. Add code + and objects here that are needed for the duration of the test() methods + in this class. In other words, set up the 'global' test environment + here; use the `setUp()` method to set up a 'local' test environment. + This method does not have access to the class `self` reference, but it + is able to call static methods within this same class. + """ + + # Open existing FreeCAD document with test geometry + FreeCAD.newDocument("Unnamed") + + @classmethod + def tearDownClass(cls): + """tearDownClass()... + This method is called prior to destruction of this test class. Add + code and objects here that cleanup the test environment after the + test() methods in this class have been executed. This method does + not have access to the class `self` reference. This method is able + to call static methods within this same class. + """ + # Close geometry document without saving + FreeCAD.closeDocument(FreeCAD.ActiveDocument.Name) + + # Setup and tear down methods called before and after each unit test + def setUp(self): + """setUp()... + This method is called prior to each `test()` method. Add code and + objects here that are needed for multiple `test()` methods. + """ + self.doc = FreeCAD.ActiveDocument + self.con = FreeCAD.Console + self.docobj = FreeCAD.ActiveDocument.addObject("Path::Feature", "testpath") + reload( + postprocessor + ) # technical debt. This shouldn't be necessary but here to bypass a bug + + def tearDown(self): + """tearDown()... + This method is called after each test() method. Add cleanup instructions here. + Such cleanup instructions will likely undo those in the setUp() method. + """ + FreeCAD.ActiveDocument.removeObject("testpath") + + def test000(self): + """Test Output Generation. + Empty path. Produces only the preamble and postable. + """ + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + # Test generating with header + # Header contains a time stamp that messes up unit testing. Only test + # length of result. + args = "--no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertTrue(len(gcode.splitlines()) == 13) + + # Test without header + expected = """(Begin preamble) +G17 G90 +G21 +(Begin operation: testpath) +(Path: testpath) +(Finish operation: testpath) +(Begin postamble) +M5 +G17 G90 +M2 +""" + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + # args = ("--no-header --no-comments --no-show-editor --precision=2") + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode, expected) + + # test without comments + expected = """G17 G90 +G21 +M5 +G17 G90 +M2 +""" + + args = "--no-header --no-comments --no-show-editor" + # args = ("--no-header --no-comments --no-show-editor --precision=2") + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode, expected) + + def test010(self): + """Test command Generation. + Test Precision + Test imperial / inches + """ + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "G0 X10.000 Y20.000 Z30.000" + self.assertEqual(result, expected) + + args = "--no-header --precision=2 --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "G0 X10.00 Y20.00 Z30.00" + self.assertEqual(result, expected) + + def test020(self): + """ + Test Line Numbers + """ + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --line-numbers --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "N150 G0 X10.000 Y20.000 Z30.000" + self.assertEqual(result, expected) + + def test030(self): + """ + Test Pre-amble + """ + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + args = "--no-header --no-comments --preamble='G18 G55\n' --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[0] + self.assertEqual(result, "G18 G55") + + def test040(self): + """ + Test Post-amble + """ + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + args = "--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[-2] + self.assertEqual(result, "G0 Z50") + self.assertEqual(gcode.splitlines()[-1], "M2") + + def test050(self): + """ + Test inches + """ + + c = Path.Command("G0 X10 Y20 Z30") + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --inches --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[2], "G20") + + result = gcode.splitlines()[5] + expected = "G0 X0.3937 Y0.7874 Z1.1811" + self.assertEqual(result, expected) + + # Technical debt. The following test fails. Precision not working + # with imperial units. + + # args = ("--no-header --inches --precision=2") + # gcode = postprocessor.export(postables, "gcode.tmp", args) + # result = gcode.splitlines()[5] + # expected = "G0 X0.39 Y0.78 Z1.18 " + # self.assertEqual(result, expected) + + def test060(self): + """ + Test test modal + Suppress the command name if the same as previous + """ + c = Path.Command("G0 X10 Y20 Z30") + c1 = Path.Command("G0 X10 Y30 Z30") + + self.docobj.Path = Path.Path([c, c1]) + postables = [self.docobj] + + # + # The grbl postprocessor does not have a --modal option. + # + # args = "--no-header --modal --no-show-editor" + # gcode = postprocessor.export(postables, "gcode.tmp", args) + # result = gcode.splitlines()[6] + # expected = "X10.000 Y30.000 Z30.000 " + # self.assertEqual(result, expected) + + def test070(self): + """ + Test axis modal + Suppress the axis coordinate if the same as previous + """ + c = Path.Command("G0 X10 Y20 Z30") + c1 = Path.Command("G0 X10 Y30 Z30") + + self.docobj.Path = Path.Path([c, c1]) + postables = [self.docobj] + + # + # The grbl postprocessor does not have a --axis-modal option. + # + # args = "--no-header --axis-modal --no-show-editor" + # gcode = postprocessor.export(postables, "gcode.tmp", args) + # result = gcode.splitlines()[6] + # expected = "G0 Y30.000 " + # self.assertEqual(result, expected) + + def test080(self): + """ + Test tool change + """ + c = Path.Command("M6 T2") + c2 = Path.Command("M3 S3000") + self.docobj.Path = Path.Path([c, c2]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[6], "( M6 T2 )") + self.assertEqual(gcode.splitlines()[7], "M3 S3000") + + # suppress TLO + # + # The grbl postprocessor does not have a --no-tlo option. + # + # args = "--no-header --no-tlo --no-show-editor" + # gcode = postprocessor.export(postables, "gcode.tmp", args) + # self.assertEqual(gcode.splitlines()[7], "M3 S3000 ") + + def test090(self): + """ + Test comment + """ + + c = Path.Command("(comment)") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "(comment)" + self.assertEqual(result, expected) diff --git a/src/Mod/Path/PathTests/TestLinuxCNCPost.py b/src/Mod/Path/PathTests/TestLinuxCNCPost.py new file mode 100644 index 0000000000..003865ba00 --- /dev/null +++ b/src/Mod/Path/PathTests/TestLinuxCNCPost.py @@ -0,0 +1,290 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2022 sliptonic * +# * * +# * 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 Part +import Path +import PathScripts.PathLog as PathLog +import PathTests.PathTestUtils as PathTestUtils +from importlib import reload +from PathScripts.post import linuxcnc_post as postprocessor + + +PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +PathLog.trackModule(PathLog.thisModule()) + + +class TestLinuxCNCPost(PathTestUtils.PathTestBase): + @classmethod + def setUpClass(cls): + """setUpClass()... + This method is called upon instantiation of this test class. Add code + and objects here that are needed for the duration of the test() methods + in this class. In other words, set up the 'global' test environment + here; use the `setUp()` method to set up a 'local' test environment. + This method does not have access to the class `self` reference, but it + is able to call static methods within this same class. + """ + + # Open existing FreeCAD document with test geometry + FreeCAD.newDocument("Unnamed") + + @classmethod + def tearDownClass(cls): + """tearDownClass()... + This method is called prior to destruction of this test class. Add + code and objects here that cleanup the test environment after the + test() methods in this class have been executed. This method does + not have access to the class `self` reference. This method is able + to call static methods within this same class. + """ + # Close geometry document without saving + FreeCAD.closeDocument(FreeCAD.ActiveDocument.Name) + + # Setup and tear down methods called before and after each unit test + def setUp(self): + """setUp()... + This method is called prior to each `test()` method. Add code and + objects here that are needed for multiple `test()` methods. + """ + self.doc = FreeCAD.ActiveDocument + self.con = FreeCAD.Console + self.docobj = FreeCAD.ActiveDocument.addObject("Path::Feature", "testpath") + reload( + postprocessor + ) # technical debt. This shouldn't be necessary but here to bypass a bug + + def tearDown(self): + """tearDown()... + This method is called after each test() method. Add cleanup instructions here. + Such cleanup instructions will likely undo those in the setUp() method. + """ + FreeCAD.ActiveDocument.removeObject("testpath") + + def test000(self): + """Test Output Generation. + Empty path. Produces only the preamble and postable. + """ + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + # Test generating with header + # Header contains a time stamp that messes up unit testing. + # Only test length of result. + args = "--no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertTrue(len(gcode.splitlines()) == 13) + + # Test without header + expected = """(begin preamble) +G17 G54 G40 G49 G80 G90 +G21 +(begin operation: testpath) +(machine units: mm/min) +(finish operation: testpath) +(begin postamble) +M05 +G17 G54 G90 G80 G40 +M2 +""" + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + # args = ("--no-header --no-comments --no-show-editor --precision=2") + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode, expected) + + # test without comments + expected = """G17 G54 G40 G49 G80 G90 +G21 +M05 +G17 G54 G90 G80 G40 +M2 +""" + + args = "--no-header --no-comments --no-show-editor" + # args = ("--no-header --no-comments --no-show-editor --precision=2") + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode, expected) + + def test010(self): + """Test command Generation. + Test Precision + Test imperial / inches + """ + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "G0 X10.000 Y20.000 Z30.000 " + self.assertEqual(result, expected) + + args = "--no-header --precision=2 --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "G0 X10.00 Y20.00 Z30.00 " + self.assertEqual(result, expected) + + def test020(self): + """ + Test Line Numbers + """ + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --line-numbers --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "N160 G0 X10.000 Y20.000 Z30.000 " + self.assertEqual(result, expected) + + def test030(self): + """ + Test Pre-amble + """ + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + args = "--no-header --no-comments --preamble='G18 G55' --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[0] + self.assertEqual(result, "G18 G55") + + def test040(self): + """ + Test Post-amble + """ + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + args = "--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[-2] + self.assertEqual(result, "G0 Z50") + self.assertEqual(gcode.splitlines()[-1], "M2") + + def test050(self): + """ + Test inches + """ + + c = Path.Command("G0 X10 Y20 Z30") + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --inches --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[2], "G20") + + result = gcode.splitlines()[5] + expected = "G0 X0.3937 Y0.7874 Z1.1811 " + self.assertEqual(result, expected) + + # Technical debt. The following test fails. Precision not working + # with imperial units. + + # args = ("--no-header --inches --precision=2") + # gcode = postprocessor.export(postables, "gcode.tmp", args) + # result = gcode.splitlines()[5] + # expected = "G0 X0.39 Y0.78 Z1.18 " + # self.assertEqual(result, expected) + + def test060(self): + """ + Test test modal + Suppress the command name if the same as previous + """ + c = Path.Command("G0 X10 Y20 Z30") + c1 = Path.Command("G0 X10 Y30 Z30") + + self.docobj.Path = Path.Path([c, c1]) + postables = [self.docobj] + + args = "--no-header --modal --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[6] + expected = "X10.000 Y30.000 Z30.000 " + self.assertEqual(result, expected) + + def test070(self): + """ + Test axis modal + Suppress the axis coordinate if the same as previous + """ + c = Path.Command("G0 X10 Y20 Z30") + c1 = Path.Command("G0 X10 Y30 Z30") + + self.docobj.Path = Path.Path([c, c1]) + postables = [self.docobj] + + args = "--no-header --axis-modal --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[6] + expected = "G0 Y30.000 " + self.assertEqual(result, expected) + + def test080(self): + """ + Test tool change + """ + c = Path.Command("M6 T2") + c2 = Path.Command("M3 S3000") + self.docobj.Path = Path.Path([c, c2]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[5], "M5") + self.assertEqual(gcode.splitlines()[6], "M6 T2 ") + self.assertEqual(gcode.splitlines()[7], "G43 H2 ") + self.assertEqual(gcode.splitlines()[8], "M3 S3000 ") + + # suppress TLO + args = "--no-header --no-tlo --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[7], "M3 S3000 ") + + def test090(self): + """ + Test comment + """ + + c = Path.Command("(comment)") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "(comment) " + self.assertEqual(result, expected) diff --git a/src/Mod/Path/PathTests/TestMach3Mach4Post.py b/src/Mod/Path/PathTests/TestMach3Mach4Post.py new file mode 100644 index 0000000000..3902734e06 --- /dev/null +++ b/src/Mod/Path/PathTests/TestMach3Mach4Post.py @@ -0,0 +1,291 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2022 sliptonic * +# * Copyright (c) 2022 Larry Woestman * +# * * +# * 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 * +# * * +# *************************************************************************** + +from importlib import reload + +import FreeCAD + +# import Part +import Path +import PathScripts.PathLog as PathLog +import PathTests.PathTestUtils as PathTestUtils +from PathScripts.post import mach3_mach4_post as postprocessor + + +PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +PathLog.trackModule(PathLog.thisModule()) + + +class TestMach3Mach4Post(PathTestUtils.PathTestBase): + @classmethod + def setUpClass(cls): + """setUpClass()... + This method is called upon instantiation of this test class. Add code + and objects here that are needed for the duration of the test() methods + in this class. In other words, set up the 'global' test environment + here; use the `setUp()` method to set up a 'local' test environment. + This method does not have access to the class `self` reference, but it + is able to call static methods within this same class. + """ + + # Open existing FreeCAD document with test geometry + FreeCAD.newDocument("Unnamed") + + @classmethod + def tearDownClass(cls): + """tearDownClass()... + This method is called prior to destruction of this test class. Add + code and objects here that cleanup the test environment after the + test() methods in this class have been executed. This method does + not have access to the class `self` reference. This method is able + to call static methods within this same class. + """ + # Close geometry document without saving + FreeCAD.closeDocument(FreeCAD.ActiveDocument.Name) + + # Setup and tear down methods called before and after each unit test + def setUp(self): + """setUp()... + This method is called prior to each `test()` method. Add code and + objects here that are needed for multiple `test()` methods. + """ + self.doc = FreeCAD.ActiveDocument + self.con = FreeCAD.Console + self.docobj = FreeCAD.ActiveDocument.addObject("Path::Feature", "testpath") + reload( + postprocessor + ) # technical debt. This shouldn't be necessary but here to bypass a bug + + def tearDown(self): + """tearDown()... + This method is called after each test() method. Add cleanup instructions here. + Such cleanup instructions will likely undo those in the setUp() method. + """ + FreeCAD.ActiveDocument.removeObject("testpath") + + def test000(self): + """Test Output Generation. + Empty path. Produces only the preamble and postable. + """ + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + # Test generating with header + # Header contains a time stamp that messes up unit testing. + # Only test length of result. + args = "--no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertTrue(len(gcode.splitlines()) == 13) + + # Test without header + expected = """(begin preamble) +G17 G54 G40 G49 G80 G90 +G21 +(begin operation: testpath) +(machine: mach3_4, mm/min) +(finish operation: testpath) +(begin postamble) +M05 +G17 G54 G90 G80 G40 +M2 +""" + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + # args = ("--no-header --no-comments --no-show-editor --precision=2") + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode, expected) + + # test without comments + expected = """G17 G54 G40 G49 G80 G90 +G21 +M05 +G17 G54 G90 G80 G40 +M2 +""" + + args = "--no-header --no-comments --no-show-editor" + # args = ("--no-header --no-comments --no-show-editor --precision=2") + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode, expected) + + def test010(self): + """Test command Generation. + Test Precision + """ + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "G0 X10.000 Y20.000 Z30.000" + self.assertEqual(result, expected) + + args = "--no-header --precision=2 --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "G0 X10.00 Y20.00 Z30.00" + self.assertEqual(result, expected) + + def test020(self): + """ + Test Line Numbers + """ + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --line-numbers --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "N160 G0 X10.000 Y20.000 Z30.000" + self.assertEqual(result, expected) + + def test030(self): + """ + Test Pre-amble + """ + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + args = "--no-header --no-comments --preamble='G18 G55' --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[0] + self.assertEqual(result, "G18 G55") + + def test040(self): + """ + Test Post-amble + """ + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + args = "--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[-2] + self.assertEqual(result, "G0 Z50") + self.assertEqual(gcode.splitlines()[-1], "M2") + + def test050(self): + """ + Test inches + """ + + c = Path.Command("G0 X10 Y20 Z30") + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --inches --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[2], "G20") + + result = gcode.splitlines()[5] + expected = "G0 X0.3937 Y0.7874 Z1.1811" + self.assertEqual(result, expected) + + # Technical debt. The following test fails. Precision not working + # with imperial units. + + # args = ("--no-header --inches --precision=2 --no-show-editor") + # gcode = postprocessor.export(postables, "gcode.tmp", args) + # result = gcode.splitlines()[5] + # expected = "G0 X0.39 Y0.79 Z1.18" + # self.assertEqual(result, expected) + + def test060(self): + """ + Test test modal + Suppress the command name if the same as previous + """ + c = Path.Command("G0 X10 Y20 Z30") + c1 = Path.Command("G0 X10 Y30 Z30") + + self.docobj.Path = Path.Path([c, c1]) + postables = [self.docobj] + + args = "--no-header --modal --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[6] + expected = "X10.000 Y30.000 Z30.000" + self.assertEqual(result, expected) + + def test070(self): + """ + Test axis modal + Suppress the axis coordinate if the same as previous + """ + c = Path.Command("G0 X10 Y20 Z30") + c1 = Path.Command("G0 X10 Y30 Z30") + + self.docobj.Path = Path.Path([c, c1]) + postables = [self.docobj] + + args = "--no-header --axis-modal --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[6] + expected = "G0 Y30.000" + self.assertEqual(result, expected) + + def test080(self): + """ + Test tool change + """ + c = Path.Command("M6 T2") + c2 = Path.Command("M3 S3000") + self.docobj.Path = Path.Path([c, c2]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[5], "M5") + self.assertEqual(gcode.splitlines()[6], "M6 T2 ") + self.assertEqual(gcode.splitlines()[7], "G43 H2") + self.assertEqual(gcode.splitlines()[8], "M3 S3000") + + # suppress TLO + args = "--no-header --no-tlo --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[7], "M3 S3000") + + def test090(self): + """ + Test comment + """ + + c = Path.Command("(comment)") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "(comment)" + self.assertEqual(result, expected) diff --git a/src/Mod/Path/PathTests/TestPathHelixGenerator.py b/src/Mod/Path/PathTests/TestPathHelixGenerator.py index b6588901b7..c8d6504ca5 100644 --- a/src/Mod/Path/PathTests/TestPathHelixGenerator.py +++ b/src/Mod/Path/PathTests/TestPathHelixGenerator.py @@ -80,7 +80,6 @@ G2 I-7.500000 J0.000000 X-2.500000 Y5.000000 Z18.000000\ G2 I7.500000 J0.000000 X12.500000 Y5.000000 Z18.000000\ G0 X5.000000 Y5.000000 Z18.000000G0 Z20.000000" - def test00(self): """Test Basic Helix Generator Return""" args = _resetArgs() @@ -118,7 +117,8 @@ G0 X5.000000 Y5.000000 Z18.000000G0 Z20.000000" args["tool_diameter"] = 5.0 self.assertRaises(ValueError, generator.generate, **args) - # require tool fit 2: hole diameter not greater than tool diam with zero inner radius + # require tool fit 2: hole diameter not greater than tool diam + # with zero inner radius args["hole_radius"] = 2.0 args["inner_radius"] = 0.0 args["tool_diameter"] = 5.0 diff --git a/src/Mod/Path/PathTests/TestPathPost.py b/src/Mod/Path/PathTests/TestPathPost.py index c6bb4ebad6..3d980fc778 100644 --- a/src/Mod/Path/PathTests/TestPathPost.py +++ b/src/Mod/Path/PathTests/TestPathPost.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # *************************************************************************** # * Copyright (c) 2016 sliptonic * +# * Copyright (c) 2022 Larry Woestman * # * * # * This program is free software; you can redistribute it and/or modify * # * it under the terms of the GNU Lesser General Public License (LGPL) * @@ -20,117 +21,199 @@ # * * # *************************************************************************** -import FreeCAD -import PathScripts -import PathScripts.post -import PathScripts.PathProfileContour -import PathScripts.PathJob -import PathScripts.PathPost as PathPost -import PathScripts.PathToolController -import PathScripts.PathUtil -import PathScripts.PostUtils as PostUtils import difflib -import unittest -import Path import os -import PathScripts.PathPost as PathPost +import unittest -WriteDebugOutput = False +import FreeCAD +import Path + +from PathScripts import PathLog +from PathScripts import PathPost +from PathScripts import PathPreferences +from PathScripts import PostUtils + +from PathScripts.PathPostProcessor import PostProcessor + +# If KEEP_DEBUG_OUTPUT is False, remove the gcode file after the test succeeds. +# If KEEP_DEBUG_OUTPUT is True or the test fails leave the gcode file behind +# so it can be looked at easily. +KEEP_DEBUG_OUTPUT = False + +PathPost.LOG_MODULE = PathLog.thisModule() +PathLog.setLevel(PathLog.Level.INFO, PathPost.LOG_MODULE) -class PathPostTestCases(unittest.TestCase): +class TestPathPost(unittest.TestCase): + """Test some of the output of the postprocessors. + + So far there are three tests each for the linuxcnc + and centroid postprocessors. + """ + def setUp(self): - testfile = FreeCAD.getHomePath() + "Mod/Path/PathTests/boxtest.fcstd" - self.doc = FreeCAD.open(testfile) - self.job = FreeCAD.ActiveDocument.getObject("Job") - self.postlist = [] - currTool = None - for obj in self.job.Group: - if not isinstance(obj.Proxy, PathScripts.PathToolController.ToolController): - tc = PathScripts.PathUtil.toolControllerForOp(obj) - if tc is not None: - if tc.ToolNumber != currTool: - self.postlist.append(tc) - self.postlist.append(obj) + """Set up the postprocessor tests.""" + pass def tearDown(self): - FreeCAD.closeDocument("boxtest") + """Tear down after the postprocessor tests.""" + pass - def testLinuxCNC(self): - from PathScripts.post import linuxcnc_post as postprocessor - - args = ( - "--no-header --no-line-numbers --no-comments --no-show-editor --precision=2" + # + # You can run just this test using: + # ./FreeCAD -c -t PathTests.TestPathPost.TestPathPost.test_postprocessors + # + def test_postprocessors(self): + """Test the postprocessors.""" + # + # The tests are performed in the order they are listed: + # one test performed on all of the postprocessors + # then the next test on all of the postprocessors, etc. + # You can comment out the tuples for tests that you don't want + # to use. + # + tests_to_perform = ( + # (output_file_id, freecad_document, job_name, postprocessor_arguments, + # postprocessor_list) + # + # test with all of the defaults (metric mode, etc.) + ("default", "boxtest1", "Job", "--no-show-editor", ()), + # test in Imperial mode + ("imperial", "boxtest1", "Job", "--no-show-editor --inches", ()), + # test in metric, G55, M4, the other way around the part + ("other_way", "boxtest1", "Job001", "--no-show-editor", ()), + # test in metric, split by fixtures, G54, G55, G56 + ("split", "boxtest1", "Job002", "--no-show-editor", ()), + # test in metric mode without the header + ("no_header", "boxtest1", "Job", "--no-header --no-show-editor", ()), + # test translating G81, G82, and G83 to G00 and G01 commands + ( + "drill_translate", + "drill_test1", + "Job", + "--no-show-editor --translate_drill", + ("grbl", "refactored_grbl"), + ), ) - gcode = postprocessor.export(self.postlist, "gcode.tmp", args) - - referenceFile = ( - FreeCAD.getHomePath() + "Mod/Path/PathTests/test_linuxcnc_00.ngc" + # + # The postprocessors to test. + # You can comment out any postprocessors that you don't want + # to test. + # + postprocessors_to_test = ( + "centroid", + # "fanuc", + "grbl", + "linuxcnc", + "mach3_mach4", + "refactored_centroid", + # "refactored_fanuc", + "refactored_grbl", + "refactored_linuxcnc", + "refactored_mach3_mach4", + "refactored_test", ) - with open(referenceFile, "r") as fp: - refGCode = fp.read() - - # Use if this test fails in order to have a real good look at the changes - if WriteDebugOutput: - with open("testLinuxCNC.tmp", "w") as fp: - fp.write(gcode) - - if gcode != refGCode: - msg = "".join( - difflib.ndiff(gcode.splitlines(True), refGCode.splitlines(True)) - ) - self.fail("linuxcnc output doesn't match: " + msg) - - def testLinuxCNCImperial(self): - from PathScripts.post import linuxcnc_post as postprocessor - - args = "--no-header --no-line-numbers --no-comments --no-show-editor --precision=2 --inches" - gcode = postprocessor.export(self.postlist, "gcode.tmp", args) - - referenceFile = ( - FreeCAD.getHomePath() + "Mod/Path/PathTests/test_linuxcnc_10.ngc" - ) - with open(referenceFile, "r") as fp: - refGCode = fp.read() - - # Use if this test fails in order to have a real good look at the changes - if WriteDebugOutput: - with open("testLinuxCNCImplerial.tmp", "w") as fp: - fp.write(gcode) - - if gcode != refGCode: - msg = "".join( - difflib.ndiff(gcode.splitlines(True), refGCode.splitlines(True)) - ) - self.fail("linuxcnc output doesn't match: " + msg) - - def testCentroid(self): - from PathScripts.post import centroid_post as postprocessor - - args = "--no-header --no-line-numbers --no-comments --no-show-editor --axis-precision=2 --feed-precision=2" - gcode = postprocessor.export(self.postlist, "gcode.tmp", args) - - referenceFile = ( - FreeCAD.getHomePath() + "Mod/Path/PathTests/test_centroid_00.ngc" - ) - with open(referenceFile, "r") as fp: - refGCode = fp.read() - - # Use if this test fails in order to have a real good look at the changes - if WriteDebugOutput: - with open("testCentroid.tmp", "w") as fp: - fp.write(gcode) - - if gcode != refGCode: - msg = "".join( - difflib.ndiff(gcode.splitlines(True), refGCode.splitlines(True)) - ) - self.fail("linuxcnc output doesn't match: " + msg) + # + # Enough of the path to where the tests are stored so that + # they can be found by the python interpreter. + # + PATHTESTS_LOCATION = "Mod/Path/PathTests" + # + # The following code tries to re-use an open FreeCAD document + # as much as possible. It compares the current document with + # the document for the next test. If the names are different + # then the current document is closed and the new document is + # opened. The final document is closed at the end of the code. + # + current_document = "" + for ( + output_file_id, + freecad_document, + job_name, + postprocessor_arguments, + postprocessor_list, + ) in tests_to_perform: + if current_document != freecad_document: + if current_document != "": + FreeCAD.closeDocument(current_document) + current_document = freecad_document + current_document_path = ( + FreeCAD.getHomePath() + + PATHTESTS_LOCATION + + os.path.sep + + current_document + + ".fcstd" + ) + FreeCAD.open(current_document_path) + job = FreeCAD.ActiveDocument.getObject(job_name) + # Create the objects to be written by the postprocessor. + postlist = PathPost.buildPostList(job) + for postprocessor_id in postprocessors_to_test: + if postprocessor_list == () or postprocessor_id in postprocessor_list: + print( + "\nRunning %s test on %s postprocessor:\n" + % (output_file_id, postprocessor_id) + ) + processor = PostProcessor.load(postprocessor_id) + output_file_path = FreeCAD.getHomePath() + PATHTESTS_LOCATION + output_file_pattern = "test_%s_%s" % ( + postprocessor_id, + output_file_id, + ) + output_file_extension = ".ngc" + for idx, section in enumerate(postlist): + partname = section[0] + sublist = section[1] + output_filename = PathPost.processFileNameSubstitutions( + job, + partname, + idx, + output_file_path, + output_file_pattern, + output_file_extension, + ) + # print("output file: " + output_filename) + file_path, extension = os.path.splitext(output_filename) + reference_file_name = "%s%s%s" % (file_path, "_ref", extension) + # print("reference file: " + reference_file_name) + gcode = processor.export( + sublist, output_filename, postprocessor_arguments + ) + if not gcode: + print("no gcode") + with open(reference_file_name, "r") as fp: + reference_gcode = fp.read() + if not reference_gcode: + print("no reference gcode") + # Remove the "Output Time:" line in the header from the + # comparison if it is present because it changes with + # every test. + gcode_lines = [ + i for i in gcode.splitlines(True) if "Output Time:" not in i + ] + reference_gcode_lines = [ + i + for i in reference_gcode.splitlines(True) + if "Output Time:" not in i + ] + if gcode_lines != reference_gcode_lines: + msg = "".join( + difflib.ndiff(gcode_lines, reference_gcode_lines) + ) + self.fail( + os.path.basename(output_filename) + + " output doesn't match:\n" + + msg + ) + if not KEEP_DEBUG_OUTPUT: + os.remove(output_filename) + if current_document != "": + FreeCAD.closeDocument(current_document) class TestPathPostUtils(unittest.TestCase): def test010(self): - + """Test the utility functions in the PostUtils.py file.""" commands = [ Path.Command("G1 X-7.5 Y5.0 Z0.0"), Path.Command("G2 I2.5 J0.0 K0.0 X-5.0 Y7.5 Z0.0"), @@ -194,7 +277,6 @@ class TestBuildPostList(unittest.TestCase): def tearDown(self): pass - def test000(self): # check that the test file is structured correctly @@ -323,12 +405,14 @@ class TestOutputNameSubstitution(unittest.TestCase): job = doc.getObjectsByLabel("MainJob")[0] macro = FreeCAD.getUserMacroDir() - def test000(self): # Test basic name generation with empty string FreeCAD.setActiveDocument(self.doc.Label) teststring = "" self.job.PostProcessorOutputFile = teststring + PathPreferences.setOutputFileDefaults( + teststring, "Append Unique ID on conflict" + ) self.job.SplitOutput = False outlist = PathPost.buildPostList(self.job) @@ -342,6 +426,9 @@ class TestOutputNameSubstitution(unittest.TestCase): # Test basic string substitution without splitting teststring = "~/Desktop/%j.nc" self.job.PostProcessorOutputFile = teststring + PathPreferences.setOutputFileDefaults( + teststring, "Append Unique ID on conflict" + ) self.job.SplitOutput = False outlist = PathPost.buildPostList(self.job) @@ -349,20 +436,31 @@ class TestOutputNameSubstitution(unittest.TestCase): subpart, objs = outlist[0] filename = PathPost.resolveFileName(self.job, subpart, 0) - self.assertEqual(filename, "~/Desktop/MainJob.nc") + self.assertEqual( + os.path.normpath(filename), os.path.normpath("~/Desktop/MainJob.nc") + ) def test010(self): # Substitute current file path teststring = "%D/testfile.nc" self.job.PostProcessorOutputFile = teststring + PathPreferences.setOutputFileDefaults( + teststring, "Append Unique ID on conflict" + ) outlist = PathPost.buildPostList(self.job) subpart, objs = outlist[0] filename = PathPost.resolveFileName(self.job, subpart, 0) - self.assertEqual(filename, f"{self.testfilepath}/testfile.nc") + self.assertEqual( + os.path.normpath(filename), + os.path.normpath(f"{self.testfilepath}/testfile.nc"), + ) def test020(self): teststring = "%d.nc" self.job.PostProcessorOutputFile = teststring + PathPreferences.setOutputFileDefaults( + teststring, "Append Unique ID on conflict" + ) outlist = PathPost.buildPostList(self.job) subpart, objs = outlist[0] filename = PathPost.resolveFileName(self.job, subpart, 0) @@ -371,6 +469,9 @@ class TestOutputNameSubstitution(unittest.TestCase): def test030(self): teststring = "%M/outfile.nc" self.job.PostProcessorOutputFile = teststring + PathPreferences.setOutputFileDefaults( + teststring, "Append Unique ID on conflict" + ) outlist = PathPost.buildPostList(self.job) subpart, objs = outlist[0] filename = PathPost.resolveFileName(self.job, subpart, 0) @@ -380,15 +481,24 @@ class TestOutputNameSubstitution(unittest.TestCase): # unused substitution strings should be ignored teststring = "%d%T%t%W%O/testdoc.nc" self.job.PostProcessorOutputFile = teststring + PathPreferences.setOutputFileDefaults( + teststring, "Append Unique ID on conflict" + ) outlist = PathPost.buildPostList(self.job) subpart, objs = outlist[0] filename = PathPost.resolveFileName(self.job, subpart, 0) - self.assertEqual(filename, f"{self.testfilename}/testdoc.nc") + self.assertEqual( + os.path.normpath(filename), + os.path.normpath(f"{self.testfilename}/testdoc.nc"), + ) def test050(self): # explicitly using the sequence number should include it where indicated. teststring = "%S-%d.nc" self.job.PostProcessorOutputFile = teststring + PathPreferences.setOutputFileDefaults( + teststring, "Append Unique ID on conflict" + ) outlist = PathPost.buildPostList(self.job) subpart, objs = outlist[0] filename = PathPost.resolveFileName(self.job, subpart, 0) @@ -403,6 +513,9 @@ class TestOutputNameSubstitution(unittest.TestCase): # substitute jobname and use default sequence numbers teststring = "%j.nc" self.job.PostProcessorOutputFile = teststring + PathPreferences.setOutputFileDefaults( + teststring, "Append Unique ID on conflict" + ) subpart, objs = outlist[0] filename = PathPost.resolveFileName(self.job, subpart, 0) self.assertEqual(filename, "MainJob-0.nc") @@ -413,6 +526,9 @@ class TestOutputNameSubstitution(unittest.TestCase): # Use Toolnumbers and default sequence numbers teststring = "%T.nc" self.job.PostProcessorOutputFile = teststring + PathPreferences.setOutputFileDefaults( + teststring, "Append Unique ID on conflict" + ) outlist = PathPost.buildPostList(self.job) subpart, objs = outlist[0] filename = PathPost.resolveFileName(self.job, subpart, 0) @@ -424,6 +540,9 @@ class TestOutputNameSubstitution(unittest.TestCase): # Use Tooldescriptions and default sequence numbers teststring = "%t.nc" self.job.PostProcessorOutputFile = teststring + PathPreferences.setOutputFileDefaults( + teststring, "Append Unique ID on conflict" + ) outlist = PathPost.buildPostList(self.job) subpart, objs = outlist[0] filename = PathPost.resolveFileName(self.job, subpart, 0) @@ -440,6 +559,9 @@ class TestOutputNameSubstitution(unittest.TestCase): teststring = "%j.nc" self.job.PostProcessorOutputFile = teststring + PathPreferences.setOutputFileDefaults( + teststring, "Append Unique ID on conflict" + ) subpart, objs = outlist[0] filename = PathPost.resolveFileName(self.job, subpart, 0) self.assertEqual(filename, "MainJob-0.nc") @@ -449,6 +571,9 @@ class TestOutputNameSubstitution(unittest.TestCase): teststring = "%W-%j.nc" self.job.PostProcessorOutputFile = teststring + PathPreferences.setOutputFileDefaults( + teststring, "Append Unique ID on conflict" + ) subpart, objs = outlist[0] filename = PathPost.resolveFileName(self.job, subpart, 0) self.assertEqual(filename, "G54-MainJob-0.nc") @@ -464,6 +589,9 @@ class TestOutputNameSubstitution(unittest.TestCase): teststring = "%j.nc" self.job.PostProcessorOutputFile = teststring + PathPreferences.setOutputFileDefaults( + teststring, "Append Unique ID on conflict" + ) subpart, objs = outlist[0] filename = PathPost.resolveFileName(self.job, subpart, 0) self.assertEqual(filename, "MainJob-0.nc") @@ -473,6 +601,9 @@ class TestOutputNameSubstitution(unittest.TestCase): teststring = "%O-%j.nc" self.job.PostProcessorOutputFile = teststring + PathPreferences.setOutputFileDefaults( + teststring, "Append Unique ID on conflict" + ) subpart, objs = outlist[0] filename = PathPost.resolveFileName(self.job, subpart, 0) self.assertEqual(filename, "OutsideProfile-MainJob-0.nc") diff --git a/src/Mod/Path/PathTests/TestRefactoredCentroidPost.py b/src/Mod/Path/PathTests/TestRefactoredCentroidPost.py new file mode 100644 index 0000000000..4c49c03167 --- /dev/null +++ b/src/Mod/Path/PathTests/TestRefactoredCentroidPost.py @@ -0,0 +1,290 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2022 sliptonic * +# * Copyright (c) 2022 Larry Woestman * +# * * +# * 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 * +# * * +# *************************************************************************** + +from importlib import reload + +import FreeCAD + +# import Part +import Path +import PathScripts.PathLog as PathLog +import PathTests.PathTestUtils as PathTestUtils +from PathScripts.post import refactored_centroid_post as postprocessor + + +PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +PathLog.trackModule(PathLog.thisModule()) + + +class TestRefactoredCentroidPost(PathTestUtils.PathTestBase): + @classmethod + def setUpClass(cls): + """setUpClass()... + This method is called upon instantiation of this test class. Add code + and objects here that are needed for the duration of the test() methods + in this class. In other words, set up the 'global' test environment + here; use the `setUp()` method to set up a 'local' test environment. + This method does not have access to the class `self` reference, but it + is able to call static methods within this same class. + """ + + # Open existing FreeCAD document with test geometry + FreeCAD.newDocument("Unnamed") + + @classmethod + def tearDownClass(cls): + """tearDownClass()... + This method is called prior to destruction of this test class. Add + code and objects here that cleanup the test environment after the + test() methods in this class have been executed. This method does not + have access to the class `self` reference. This method is able to + call static methods within this same class. + """ + # Close geometry document without saving + FreeCAD.closeDocument(FreeCAD.ActiveDocument.Name) + + # Setup and tear down methods called before and after each unit test + def setUp(self): + """setUp()... + This method is called prior to each `test()` method. Add code and + objects here that are needed for multiple `test()` methods. + """ + self.doc = FreeCAD.ActiveDocument + self.con = FreeCAD.Console + self.docobj = FreeCAD.ActiveDocument.addObject("Path::Feature", "testpath") + reload( + postprocessor + ) # technical debt. This shouldn't be necessary but here to bypass a bug + + def tearDown(self): + """tearDown()... + This method is called after each test() method. Add cleanup instructions here. + Such cleanup instructions will likely undo those in the setUp() method. + """ + FreeCAD.ActiveDocument.removeObject("testpath") + + def test000(self): + """Test Output Generation. + Empty path. Produces only the preamble and postable. + """ + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + # Test generating with header + # Header contains a time stamp that messes up unit testing. + # Only test length of result. + args = "--no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertTrue(len(gcode.splitlines()) == 16) + + # Test without header + expected = """G90 G80 G40 G49 +;Begin preamble +G53 G00 G17 +G21 +;Begin operation +;End operation: testpath +;Begin postamble +M5 +M25 +G49 H0 +G90 G80 G40 G49 +M99 +""" + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode, expected) + + # test without comments + expected = """G90 G80 G40 G49 +G53 G00 G17 +G21 +M5 +M25 +G49 H0 +G90 G80 G40 G49 +M99 +""" + + args = "--no-header --no-comments --no-show-editor" + # args = ("--no-header --no-comments --no-show-editor --precision=2") + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode, expected) + + def test010(self): + """Test command Generation. + Test Precision + """ + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "G0 X10.0000 Y20.0000 Z30.0000" + self.assertEqual(result, expected) + + args = "--no-header --axis-precision=2 --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "G0 X10.00 Y20.00 Z30.00" + self.assertEqual(result, expected) + + def test020(self): + """ + Test Line Numbers + """ + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --line-numbers --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "N150 G0 X10.0000 Y20.0000 Z30.0000" + self.assertEqual(result, expected) + + def test030(self): + """ + Test Pre-amble + """ + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + args = "--no-header --no-comments --preamble='G18 G55' --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[1] + self.assertEqual(result, "G18 G55") + + def test040(self): + """ + Test Post-amble + """ + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + args = "--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[-2] + self.assertEqual(result, "G0 Z50") + self.assertEqual(gcode.splitlines()[-1], "M2") + + def test050(self): + """ + Test inches + """ + + c = Path.Command("G0 X10 Y20 Z30") + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --inches --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[3], "G20") + + result = gcode.splitlines()[5] + expected = "G0 X0.3937 Y0.7874 Z1.1811" + self.assertEqual(result, expected) + + args = "--no-header --inches --axis-precision=2 --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "G0 X0.39 Y0.79 Z1.18" + self.assertEqual(result, expected) + + def test060(self): + """ + Test test modal + Suppress the command name if the same as previous + """ + c = Path.Command("G0 X10 Y20 Z30") + c1 = Path.Command("G0 X10 Y30 Z30") + + self.docobj.Path = Path.Path([c, c1]) + postables = [self.docobj] + + args = "--no-header --modal --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[6] + expected = "X10.0000 Y30.0000 Z30.0000" + self.assertEqual(result, expected) + + def test070(self): + """ + Test axis modal + Suppress the axis coordinate if the same as previous + """ + c = Path.Command("G0 X10 Y20 Z30") + c1 = Path.Command("G0 X10 Y30 Z30") + + self.docobj.Path = Path.Path([c, c1]) + postables = [self.docobj] + + args = "--no-header --axis-modal --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[6] + expected = "G0 Y30.0000" + self.assertEqual(result, expected) + + def test080(self): + """ + Test tool change + """ + c = Path.Command("M6 T2") + c2 = Path.Command("M3 S3000") + self.docobj.Path = Path.Path([c, c2]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[6], "M6 T2") + self.assertEqual(gcode.splitlines()[7], "M3 S3000") + + # suppress TLO + args = "--no-header --no-tlo --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[7], "M3 S3000") + + def test090(self): + """ + Test comment + """ + + c = Path.Command("(comment)") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = ";comment" + self.assertEqual(result, expected) diff --git a/src/Mod/Path/PathTests/TestRefactoredGrblPost.py b/src/Mod/Path/PathTests/TestRefactoredGrblPost.py new file mode 100644 index 0000000000..9affd355ca --- /dev/null +++ b/src/Mod/Path/PathTests/TestRefactoredGrblPost.py @@ -0,0 +1,286 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2022 sliptonic * +# * Copyright (c) 2022 Larry Woestman * +# * * +# * 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 * +# * * +# *************************************************************************** + +from importlib import reload + +import FreeCAD + +# import Part +import Path +import PathScripts.PathLog as PathLog +import PathTests.PathTestUtils as PathTestUtils +from PathScripts.post import refactored_grbl_post as postprocessor + + +PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +PathLog.trackModule(PathLog.thisModule()) + + +class TestRefactoredGrblPost(PathTestUtils.PathTestBase): + @classmethod + def setUpClass(cls): + """setUpClass()... + This method is called upon instantiation of this test class. Add code + and objects here that are needed for the duration of the test() methods + in this class. In other words, set up the 'global' test environment + here; use the `setUp()` method to set up a 'local' test environment. + This method does not have access to the class `self` reference, but it + is able to call static methods within this same class. + """ + + # Open existing FreeCAD document with test geometry + FreeCAD.newDocument("Unnamed") + + @classmethod + def tearDownClass(cls): + """tearDownClass()... + This method is called prior to destruction of this test class. Add + code and objects here that cleanup the test environment after the + test() methods in this class have been executed. This method does not + have access to the class `self` reference. This method + is able to call static methods within this same class. + """ + # Close geometry document without saving + FreeCAD.closeDocument(FreeCAD.ActiveDocument.Name) + + # Setup and tear down methods called before and after each unit test + def setUp(self): + """setUp()... + This method is called prior to each `test()` method. Add code and + objects here that are needed for multiple `test()` methods. + """ + self.doc = FreeCAD.ActiveDocument + self.con = FreeCAD.Console + self.docobj = FreeCAD.ActiveDocument.addObject("Path::Feature", "testpath") + reload( + postprocessor + ) # technical debt. This shouldn't be necessary but here to bypass a bug + + def tearDown(self): + """tearDown()... + This method is called after each test() method. Add cleanup instructions here. + Such cleanup instructions will likely undo those in the setUp() method. + """ + FreeCAD.ActiveDocument.removeObject("testpath") + + def test000(self): + """Test Output Generation. + Empty path. Produces only the preamble and postable. + """ + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + # Test generating with header + # Header contains a time stamp that messes up unit testing. + # Only test length of result. + args = "--no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertTrue(len(gcode.splitlines()) == 14) + + # Test without header + expected = """(Begin preamble) +G17 G90 +G21 +(Begin operation: testpath) +(Path: testpath) +(Finish operation: testpath) +(Begin postamble) +M5 +G17 G90 +M2 +""" + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + # args = ("--no-header --no-comments --no-show-editor --precision=2") + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode, expected) + + # test without comments + expected = """G17 G90 +G21 +M5 +G17 G90 +M2 +""" + + args = "--no-header --no-comments --no-show-editor" + # args = ("--no-header --no-comments --no-show-editor --precision=2") + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode, expected) + + def test010(self): + """Test command Generation. + Test Precision + """ + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "G0 X10.000 Y20.000 Z30.000" + self.assertEqual(result, expected) + + args = "--no-header --precision=2 --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "G0 X10.00 Y20.00 Z30.00" + self.assertEqual(result, expected) + + def test020(self): + """ + Test Line Numbers + """ + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --line-numbers --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "N150 G0 X10.000 Y20.000 Z30.000" + self.assertEqual(result, expected) + + def test030(self): + """ + Test Pre-amble + """ + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + args = "--no-header --no-comments --preamble='G18 G55' --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[0] + self.assertEqual(result, "G18 G55") + + def test040(self): + """ + Test Post-amble + """ + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + args = "--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[-2] + self.assertEqual(result, "G0 Z50") + self.assertEqual(gcode.splitlines()[-1], "M2") + + def test050(self): + """ + Test inches + """ + + c = Path.Command("G0 X10 Y20 Z30") + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --inches --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[2], "G20") + + result = gcode.splitlines()[5] + expected = "G0 X0.3937 Y0.7874 Z1.1811" + self.assertEqual(result, expected) + + args = "--no-header --inches --precision=2 --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "G0 X0.39 Y0.79 Z1.18" + self.assertEqual(result, expected) + + def test060(self): + """ + Test test modal + Suppress the command name if the same as previous + """ + c = Path.Command("G0 X10 Y20 Z30") + c1 = Path.Command("G0 X10 Y30 Z30") + + self.docobj.Path = Path.Path([c, c1]) + postables = [self.docobj] + + args = "--no-header --modal --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[6] + expected = "X10.000 Y30.000 Z30.000" + self.assertEqual(result, expected) + + def test070(self): + """ + Test axis modal + Suppress the axis coordinate if the same as previous + """ + c = Path.Command("G0 X10 Y20 Z30") + c1 = Path.Command("G0 X10 Y30 Z30") + + self.docobj.Path = Path.Path([c, c1]) + postables = [self.docobj] + + args = "--no-header --axis-modal --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[6] + expected = "G0 Y30.000" + self.assertEqual(result, expected) + + def test080(self): + """ + Test tool change + """ + c = Path.Command("M6 T2") + c2 = Path.Command("M3 S3000") + self.docobj.Path = Path.Path([c, c2]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[6], "( M6 T2 )") + self.assertEqual(gcode.splitlines()[7], "M3 S3000") + + # suppress TLO + args = "--no-header --no-tlo --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[7], "M3 S3000") + + def test090(self): + """ + Test comment + """ + + c = Path.Command("(comment)") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "(comment)" + self.assertEqual(result, expected) diff --git a/src/Mod/Path/PathTests/TestRefactoredLinuxCNCPost.py b/src/Mod/Path/PathTests/TestRefactoredLinuxCNCPost.py new file mode 100644 index 0000000000..267dc10f84 --- /dev/null +++ b/src/Mod/Path/PathTests/TestRefactoredLinuxCNCPost.py @@ -0,0 +1,288 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2022 sliptonic * +# * Copyright (c) 2022 Larry Woestman * +# * * +# * 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 * +# * * +# *************************************************************************** + +from importlib import reload + +import FreeCAD + +# import Part +import Path +import PathScripts.PathLog as PathLog +import PathTests.PathTestUtils as PathTestUtils +from PathScripts.post import refactored_linuxcnc_post as postprocessor + + +PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +PathLog.trackModule(PathLog.thisModule()) + + +class TestRefactoredLinuxCNCPost(PathTestUtils.PathTestBase): + @classmethod + def setUpClass(cls): + """setUpClass()... + This method is called upon instantiation of this test class. Add code + and objects here that are needed for the duration of the test() methods + in this class. In other words, set up the 'global' test environment + here; use the `setUp()` method to set up a 'local' test environment. + This method does not have access to the class `self` reference, but it + is able to call static methods within this same class. + """ + + # Open existing FreeCAD document with test geometry + FreeCAD.newDocument("Unnamed") + + @classmethod + def tearDownClass(cls): + """tearDownClass()... + This method is called prior to destruction of this test class. Add + code and objects here that cleanup the test environment after the + test() methods in this class have been executed. This method does not + have access to the class `self` reference. This method + is able to call static methods within this same class. + """ + # Close geometry document without saving + FreeCAD.closeDocument(FreeCAD.ActiveDocument.Name) + + # Setup and tear down methods called before and after each unit test + def setUp(self): + """setUp()... + This method is called prior to each `test()` method. Add code and + objects here that are needed for multiple `test()` methods. + """ + self.doc = FreeCAD.ActiveDocument + self.con = FreeCAD.Console + self.docobj = FreeCAD.ActiveDocument.addObject("Path::Feature", "testpath") + reload( + postprocessor + ) # technical debt. This shouldn't be necessary but here to bypass a bug + + def tearDown(self): + """tearDown()... + This method is called after each test() method. Add cleanup instructions here. + Such cleanup instructions will likely undo those in the setUp() method. + """ + FreeCAD.ActiveDocument.removeObject("testpath") + + def test000(self): + """Test Output Generation. + Empty path. Produces only the preamble and postable. + """ + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + # Test generating with header + # Header contains a time stamp that messes up unit testing. + # Only test length of result. + args = "--no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertTrue(len(gcode.splitlines()) == 14) + + # Test without header + expected = """(Begin preamble) +G17 G54 G40 G49 G80 G90 +G21 +(Begin operation: testpath) +(Machine units: mm/min) +(Finish operation: testpath) +(Begin postamble) +M05 +G17 G54 G90 G80 G40 +M2 +""" + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + # args = ("--no-header --no-comments --no-show-editor --precision=2") + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode, expected) + + # test without comments + expected = """G17 G54 G40 G49 G80 G90 +G21 +M05 +G17 G54 G90 G80 G40 +M2 +""" + + args = "--no-header --no-comments --no-show-editor" + # args = ("--no-header --no-comments --no-show-editor --precision=2") + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode, expected) + + def test010(self): + """Test command Generation. + Test Precision + """ + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "G0 X10.000 Y20.000 Z30.000" + self.assertEqual(result, expected) + + args = "--no-header --precision=2 --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "G0 X10.00 Y20.00 Z30.00" + self.assertEqual(result, expected) + + def test020(self): + """ + Test Line Numbers + """ + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --line-numbers --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "N150 G0 X10.000 Y20.000 Z30.000" + self.assertEqual(result, expected) + + def test030(self): + """ + Test Pre-amble + """ + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + args = "--no-header --no-comments --preamble='G18 G55' --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[0] + self.assertEqual(result, "G18 G55") + + def test040(self): + """ + Test Post-amble + """ + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + args = "--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[-2] + self.assertEqual(result, "G0 Z50") + self.assertEqual(gcode.splitlines()[-1], "M2") + + def test050(self): + """ + Test inches + """ + + c = Path.Command("G0 X10 Y20 Z30") + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --inches --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[2], "G20") + + result = gcode.splitlines()[5] + expected = "G0 X0.3937 Y0.7874 Z1.1811" + self.assertEqual(result, expected) + + args = "--no-header --inches --precision=2 --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "G0 X0.39 Y0.79 Z1.18" + self.assertEqual(result, expected) + + def test060(self): + """ + Test test modal + Suppress the command name if the same as previous + """ + c = Path.Command("G0 X10 Y20 Z30") + c1 = Path.Command("G0 X10 Y30 Z30") + + self.docobj.Path = Path.Path([c, c1]) + postables = [self.docobj] + + args = "--no-header --modal --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[6] + expected = "X10.000 Y30.000 Z30.000" + self.assertEqual(result, expected) + + def test070(self): + """ + Test axis modal + Suppress the axis coordinate if the same as previous + """ + c = Path.Command("G0 X10 Y20 Z30") + c1 = Path.Command("G0 X10 Y30 Z30") + + self.docobj.Path = Path.Path([c, c1]) + postables = [self.docobj] + + args = "--no-header --axis-modal --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[6] + expected = "G0 Y30.000" + self.assertEqual(result, expected) + + def test080(self): + """ + Test tool change + """ + c = Path.Command("M6 T2") + c2 = Path.Command("M3 S3000") + self.docobj.Path = Path.Path([c, c2]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[6], "M5") + self.assertEqual(gcode.splitlines()[7], "M6 T2") + self.assertEqual(gcode.splitlines()[8], "G43 H2") + self.assertEqual(gcode.splitlines()[9], "M3 S3000") + + # suppress TLO + args = "--no-header --no-tlo --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[8], "M3 S3000") + + def test090(self): + """ + Test comment + """ + + c = Path.Command("(comment)") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "(comment)" + self.assertEqual(result, expected) diff --git a/src/Mod/Path/PathTests/TestRefactoredMach3Mach4Post.py b/src/Mod/Path/PathTests/TestRefactoredMach3Mach4Post.py new file mode 100644 index 0000000000..dab611262d --- /dev/null +++ b/src/Mod/Path/PathTests/TestRefactoredMach3Mach4Post.py @@ -0,0 +1,287 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2022 sliptonic * +# * Copyright (c) 2022 Larry Woestman * +# * * +# * 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 * +# * * +# *************************************************************************** + +from importlib import reload + +import FreeCAD + +# import Part +import Path +import PathScripts.PathLog as PathLog +import PathTests.PathTestUtils as PathTestUtils +from PathScripts.post import refactored_mach3_mach4_post as postprocessor + +PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +PathLog.trackModule(PathLog.thisModule()) + + +class TestRefactoredMach3Mach4Post(PathTestUtils.PathTestBase): + @classmethod + def setUpClass(cls): + """setUpClass()... + This method is called upon instantiation of this test class. Add code + and objects here that are needed for the duration of the test() methods + in this class. In other words, set up the 'global' test environment + here; use the `setUp()` method to set up a 'local' test environment. + This method does not have access to the class `self` reference, but it + is able to call static methods within this same class. + """ + + # Open existing FreeCAD document with test geometry + FreeCAD.newDocument("Unnamed") + + @classmethod + def tearDownClass(cls): + """tearDownClass()... + This method is called prior to destruction of this test class. Add + code and objects here that cleanup the test environment after the + test() methods in this class have been executed. This method does not + have access to the class `self` reference. This method is able to + call static methods within this same class. + """ + # Close geometry document without saving + FreeCAD.closeDocument(FreeCAD.ActiveDocument.Name) + + # Setup and tear down methods called before and after each unit test + def setUp(self): + """setUp()... + This method is called prior to each `test()` method. Add code and + objects here that are needed for multiple `test()` methods. + """ + self.doc = FreeCAD.ActiveDocument + self.con = FreeCAD.Console + self.docobj = FreeCAD.ActiveDocument.addObject("Path::Feature", "testpath") + reload( + postprocessor + ) # technical debt. This shouldn't be necessary but here to bypass a bug + + def tearDown(self): + """tearDown()... + This method is called after each test() method. Add cleanup instructions here. + Such cleanup instructions will likely undo those in the setUp() method. + """ + FreeCAD.ActiveDocument.removeObject("testpath") + + def test000(self): + """Test Output Generation. + Empty path. Produces only the preamble and postable. + """ + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + # Test generating with header + # Header contains a time stamp that messes up unit testing. + # Only test length of result. + args = "--no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertTrue(len(gcode.splitlines()) == 14) + + # Test without header + expected = """(Begin preamble) +G17 G54 G40 G49 G80 G90 +G21 +(Begin operation: testpath) +(Machine: mach3_4, mm/min) +(Finish operation: testpath) +(Begin postamble) +M05 +G17 G54 G90 G80 G40 +M2 +""" + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + # args = ("--no-header --no-comments --no-show-editor --precision=2") + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode, expected) + + # test without comments + expected = """G17 G54 G40 G49 G80 G90 +G21 +M05 +G17 G54 G90 G80 G40 +M2 +""" + + args = "--no-header --no-comments --no-show-editor" + # args = ("--no-header --no-comments --no-show-editor --precision=2") + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode, expected) + + def test010(self): + """Test command Generation. + Test Precision + """ + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "G0 X10.000 Y20.000 Z30.000" + self.assertEqual(result, expected) + + args = "--no-header --precision=2 --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "G0 X10.00 Y20.00 Z30.00" + self.assertEqual(result, expected) + + def test020(self): + """ + Test Line Numbers + """ + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --line-numbers --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "N150 G0 X10.000 Y20.000 Z30.000" + self.assertEqual(result, expected) + + def test030(self): + """ + Test Pre-amble + """ + + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + args = "--no-header --no-comments --preamble='G18 G55' --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[0] + self.assertEqual(result, "G18 G55") + + def test040(self): + """ + Test Post-amble + """ + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + args = "--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[-2] + self.assertEqual(result, "G0 Z50") + self.assertEqual(gcode.splitlines()[-1], "M2") + + def test050(self): + """ + Test inches + """ + + c = Path.Command("G0 X10 Y20 Z30") + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --inches --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[2], "G20") + + result = gcode.splitlines()[5] + expected = "G0 X0.3937 Y0.7874 Z1.1811" + self.assertEqual(result, expected) + + args = "--no-header --inches --precision=2 --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "G0 X0.39 Y0.79 Z1.18" + self.assertEqual(result, expected) + + def test060(self): + """ + Test test modal + Suppress the command name if the same as previous + """ + c = Path.Command("G0 X10 Y20 Z30") + c1 = Path.Command("G0 X10 Y30 Z30") + + self.docobj.Path = Path.Path([c, c1]) + postables = [self.docobj] + + args = "--no-header --modal --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[6] + expected = "X10.000 Y30.000 Z30.000" + self.assertEqual(result, expected) + + def test070(self): + """ + Test axis modal + Suppress the axis coordinate if the same as previous + """ + c = Path.Command("G0 X10 Y20 Z30") + c1 = Path.Command("G0 X10 Y30 Z30") + + self.docobj.Path = Path.Path([c, c1]) + postables = [self.docobj] + + args = "--no-header --axis-modal --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[6] + expected = "G0 Y30.000" + self.assertEqual(result, expected) + + def test080(self): + """ + Test tool change + """ + c = Path.Command("M6 T2") + c2 = Path.Command("M3 S3000") + self.docobj.Path = Path.Path([c, c2]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[6], "M5") + self.assertEqual(gcode.splitlines()[7], "M6 T2") + self.assertEqual(gcode.splitlines()[8], "G43 H2") + self.assertEqual(gcode.splitlines()[9], "M3 S3000") + + # suppress TLO + args = "--no-header --no-tlo --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + self.assertEqual(gcode.splitlines()[8], "M3 S3000") + + def test090(self): + """ + Test comment + """ + + c = Path.Command("(comment)") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--no-header --no-show-editor" + gcode = postprocessor.export(postables, "gcode.tmp", args) + result = gcode.splitlines()[5] + expected = "(comment)" + self.assertEqual(result, expected) diff --git a/src/Mod/Path/PathTests/TestRefactoredTestPost.py b/src/Mod/Path/PathTests/TestRefactoredTestPost.py new file mode 100644 index 0000000000..20be8a9f19 --- /dev/null +++ b/src/Mod/Path/PathTests/TestRefactoredTestPost.py @@ -0,0 +1,1278 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2022 sliptonic * +# * Copyright (c) 2022 Larry Woestman * +# * * +# * 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 * +# * * +# *************************************************************************** + +from importlib import reload + +import FreeCAD + +# import Part +import Path +import PathScripts.PathLog as PathLog +import PathTests.PathTestUtils as PathTestUtils +from PathScripts.post import refactored_test_post as postprocessor + + +PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +PathLog.trackModule(PathLog.thisModule()) + + +class TestRefactoredTestPost(PathTestUtils.PathTestBase): + @classmethod + def setUpClass(cls): + """setUpClass()... + + This method is called upon instantiation of this test class. Add code + and objects here that are needed for the duration of the test() methods + in this class. In other words, set up the 'global' test environment + here; use the `setUp()` method to set up a 'local' test environment. + This method does not have access to the class `self` reference, but it + is able to call static methods within this same class. + """ + # Open existing FreeCAD document with test geometry + FreeCAD.newDocument("Unnamed") + + @classmethod + def tearDownClass(cls): + """tearDownClass()... + + This method is called prior to destruction of this test class. Add + code and objects here that cleanup the test environment after the + test() methods in this class have been executed. This method does + not have access to the class `self` reference. This method is able + to call static methods within this same class. + """ + # Close geometry document without saving + FreeCAD.closeDocument(FreeCAD.ActiveDocument.Name) + + # Setup and tear down methods called before and after each unit test + + def setUp(self): + """setUp()... + + This method is called prior to each `test()` method. Add code and + objects here that are needed for multiple `test()` methods. + """ + self.doc = FreeCAD.ActiveDocument + self.con = FreeCAD.Console + self.docobj = FreeCAD.ActiveDocument.addObject("Path::Feature", "testpath") + reload( + postprocessor + ) # technical debt. This shouldn't be necessary but here to bypass a bug + + def tearDown(self): + """tearDown()... + + This method is called after each test() method. Add cleanup instructions here. + Such cleanup instructions will likely undo those in the setUp() method. + """ + FreeCAD.ActiveDocument.removeObject("testpath") + + def test00000(self): + """Test Output Generation. + + Empty path. Produces only the preamble and postable. + Also tests the interactions between --comments and --header. + """ + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + # Test generating with comments and header. + # The header contains a time stamp that messes up unit testing. + # Only test the length of the line that contains the time. + args = "--comments --header" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[0], "(Exported by FreeCAD)") + self.assertEqual( + gcode.splitlines()[1], + "(Post Processor: PathScripts.post.refactored_test_post)", + ) + self.assertEqual(gcode.splitlines()[2], "(Cam File: )") + self.assertIn("(Output Time: ", gcode.splitlines()[3]) + self.assertTrue(len(gcode.splitlines()[3]) == 41) + self.assertEqual(gcode.splitlines()[4], "(Begin preamble)") + self.assertEqual(gcode.splitlines()[5], "G90") + self.assertEqual(gcode.splitlines()[6], "G21") + self.assertEqual(gcode.splitlines()[7], "(Begin operation)") + self.assertEqual(gcode.splitlines()[8], "(Finish operation: testpath)") + self.assertEqual(gcode.splitlines()[9], "(Begin postamble)") + + # Test with comments without header. + expected = """(Begin preamble) +G90 +G21 +(Begin operation) +(Finish operation: testpath) +(Begin postamble) +""" + args = "--comments --no-header" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + # Test without comments with header. + args = "--no-comments --header" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[0], "(Exported by FreeCAD)") + self.assertEqual( + gcode.splitlines()[1], + "(Post Processor: PathScripts.post.refactored_test_post)", + ) + self.assertEqual(gcode.splitlines()[2], "(Cam File: )") + self.assertIn("(Output Time: ", gcode.splitlines()[3]) + self.assertTrue(len(gcode.splitlines()[3]) == 41) + self.assertEqual(gcode.splitlines()[4], "G90") + self.assertEqual(gcode.splitlines()[5], "G21") + + # Test without comments or header. + expected = """G90 +G21 +""" + args = "--no-comments --no-header" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test00100(self): + """Test bcnc.""" + # + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + expected = """G90 +G21 +(Block-name: testpath) +(Block-expand: 0) +(Block-enable: 1) +(Block-name: post_amble) +(Block-expand: 0) +(Block-enable: 1) +""" + args = "--bcnc" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + expected = """G90 +G21 +""" + args = "--no-bcnc" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test00110(self): + """Test axis modal. + + Suppress the axis coordinate if the same as previous + """ + c = Path.Command("G0 X10 Y20 Z30") + c1 = Path.Command("G0 X10 Y30 Z30") + + self.docobj.Path = Path.Path([c, c1]) + postables = [self.docobj] + + args = "--axis-modal" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[3], "G0 Y30.000") + + args = "--no-axis-modal" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[3], "G0 X10.000 Y30.000 Z30.000") + + def test00120(self): + """Test axis-precision.""" + # + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--axis-precision=2" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[2], "G0 X10.00 Y20.00 Z30.00") + + def test00130(self): + """Test comments.""" + # + c = Path.Command("(comment)") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--comments" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[4], "(comment)") + + def test00140(self): + """Test feed-precision.""" + # + c = Path.Command("G1 X10 Y20 Z30 F123.123456") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + # Note: The "internal" F speed is in mm/s, + # while the output F speed is in mm/min. + self.assertEqual(gcode.splitlines()[2], "G1 X10.000 Y20.000 Z30.000 F7387.407") + + args = "--feed-precision=2" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + # Note: The "internal" F speed is in mm/s, + # while the output F speed is in mm/min. + self.assertEqual(gcode.splitlines()[2], "G1 X10.000 Y20.000 Z30.000 F7387.41") + + def test00150(self): + """Test Line Numbers.""" + # + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--line-numbers" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[2], "N120 G0 X10.000 Y20.000 Z30.000") + + def test00160(self): + """Test inches.""" + # + c = Path.Command("G0 X10 Y20 Z30") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--inches" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[1], "G20") + self.assertEqual(gcode.splitlines()[2], "G0 X0.3937 Y0.7874 Z1.1811") + + def test00170(self): + """Test modal. + + Suppress the command name if the same as previous + """ + c = Path.Command("G0 X10 Y20 Z30") + c1 = Path.Command("G0 X10 Y30 Z30") + + self.docobj.Path = Path.Path([c, c1]) + postables = [self.docobj] + + args = "--modal" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[3], "X10.000 Y30.000 Z30.000") + + args = "--no-modal" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[3], "G0 X10.000 Y30.000 Z30.000") + + def test00180(self): + """Test Post-amble.""" + # + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + args = "--postamble='G0 Z50\nM2'" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[-2], "G0 Z50") + self.assertEqual(gcode.splitlines()[-1], "M2") + + def test00190(self): + """Test Pre-amble.""" + # + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + args = "--preamble='G18 G55'" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[0], "G18 G55") + + def test00200(self): + """Test precision.""" + # + c = Path.Command("G1 X10 Y20 Z30 F100") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "--precision=2" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[2], "G1 X10.00 Y20.00 Z30.00 F6000.00") + + args = "--inches --precision=2" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[2], "G1 X0.39 Y0.79 Z1.18 F236.22") + + def test00210(self): + """Test return-to.""" + # + self.docobj.Path = Path.Path([]) + postables = [self.docobj] + + expected = """G90 +G21 +G0 X12 Y34 Z56 +""" + args = "--return-to='12,34,56'" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test00220(self): + """Test tlo.""" + # + c = Path.Command("M6 T2") + c2 = Path.Command("M3 S3000") + + self.docobj.Path = Path.Path([c, c2]) + postables = [self.docobj] + + args = "--tlo" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[2], "M6 T2") + self.assertEqual(gcode.splitlines()[3], "G43 H2") + self.assertEqual(gcode.splitlines()[4], "M3 S3000") + + # suppress TLO + args = "--no-tlo" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[2], "M6 T2") + self.assertEqual(gcode.splitlines()[3], "M3 S3000") + + def test00230(self): + """Test tool_change.""" + # + c = Path.Command("M6 T2") + c2 = Path.Command("M3 S3000") + + self.docobj.Path = Path.Path([c, c2]) + postables = [self.docobj] + + args = "--tool_change" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[2], "M6 T2") + self.assertEqual(gcode.splitlines()[3], "M3 S3000") + + args = "--comments --no-tool_change" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[5], "( M6 T2 )") + self.assertEqual(gcode.splitlines()[6], "M3 S3000") + + def test00240(self): + """Test translate_drill with G81.""" + # + c = Path.Command("G0 X1 Y2") + c1 = Path.Command("G0 Z8") + c2 = Path.Command("G90") + c3 = Path.Command("G99") + c4 = Path.Command("G81 X1 Y2 Z0 F123 R5") + c5 = Path.Command("G80") + c6 = Path.Command("G90") + + self.docobj.Path = Path.Path([c, c1, c2, c3, c4, c5, c6]) + postables = [self.docobj] + + expected = """G90 +G21 +G0 X1.000 Y2.000 +G0 Z8.000 +G90 +G99 +G81 X1.000 Y2.000 Z0.000 F7380.000 R5.000 +G80 +G90 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + expected = """G90 +G21 +G0 X1.000 Y2.000 +G0 Z8.000 +G90 +G0 X1.000 Y2.000 +G1 Z5.000 F7380.000 +G1 Z0.000 F7380.000 +G0 Z5.000 +G90 +""" + args = "--translate_drill" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + expected = """(Begin preamble) +G90 +G21 +(Begin operation) +G0 X1.000 Y2.000 +G0 Z8.000 +G90 +( G99 ) +( G81 X1.000 Y2.000 Z0.000 F7380.000 R5.000 ) +G0 X1.000 Y2.000 +G1 Z5.000 F7380.000 +G1 Z0.000 F7380.000 +G0 Z5.000 +( G80 ) +G90 +(Finish operation: testpath) +(Begin postamble) +""" + args = "--comments --translate_drill" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test00250(self): + """Test translate_drill with G82.""" + # + c = Path.Command("G0 X1 Y2") + c1 = Path.Command("G0 Z8") + c2 = Path.Command("G90") + c3 = Path.Command("G99") + c4 = Path.Command("G82 X1 Y2 Z0 F123 R5 P1.23456") + c5 = Path.Command("G80") + c6 = Path.Command("G90") + + self.docobj.Path = Path.Path([c, c1, c2, c3, c4, c5, c6]) + postables = [self.docobj] + + expected = """G90 +G21 +G0 X1.000 Y2.000 +G0 Z8.000 +G90 +G99 +G82 X1.000 Y2.000 Z0.000 F7380.000 R5.000 P1.23456 +G80 +G90 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + expected = """G90 +G21 +G0 X1.000 Y2.000 +G0 Z8.000 +G90 +G0 X1.000 Y2.000 +G1 Z5.000 F7380.000 +G1 Z0.000 F7380.000 +G4 P1.23456 +G0 Z5.000 +G90 +""" + args = "--translate_drill" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + expected = """(Begin preamble) +G90 +G21 +(Begin operation) +G0 X1.000 Y2.000 +G0 Z8.000 +G90 +( G99 ) +( G82 X1.000 Y2.000 Z0.000 F7380.000 R5.000 P1.23456 ) +G0 X1.000 Y2.000 +G1 Z5.000 F7380.000 +G1 Z0.000 F7380.000 +G4 P1.23456 +G0 Z5.000 +( G80 ) +G90 +(Finish operation: testpath) +(Begin postamble) +""" + args = "--comments --translate_drill" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test00260(self): + """Test translate_drill with G83.""" + # + c = Path.Command("G0 X1 Y2") + c1 = Path.Command("G0 Z8") + c2 = Path.Command("G90") + c3 = Path.Command("G99") + c4 = Path.Command("G83 X1 Y2 Z0 F123 Q1.5 R5") + c5 = Path.Command("G80") + c6 = Path.Command("G90") + + self.docobj.Path = Path.Path([c, c1, c2, c3, c4, c5, c6]) + postables = [self.docobj] + + expected = """G90 +G21 +G0 X1.000 Y2.000 +G0 Z8.000 +G90 +G99 +G83 X1.000 Y2.000 Z0.000 F7380.000 Q1.500 R5.000 +G80 +G90 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + expected = """G90 +G21 +G0 X1.000 Y2.000 +G0 Z8.000 +G90 +G0 X1.000 Y2.000 +G1 Z5.000 F7380.000 +G1 Z3.500 F7380.000 +G0 Z5.000 +G0 Z3.575 +G1 Z2.000 F7380.000 +G0 Z5.000 +G0 Z2.075 +G1 Z0.500 F7380.000 +G0 Z5.000 +G0 Z0.575 +G1 Z0.000 F7380.000 +G0 Z5.000 +G90 +""" + args = "--translate_drill" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + expected = """(Begin preamble) +G90 +G21 +(Begin operation) +G0 X1.000 Y2.000 +G0 Z8.000 +G90 +( G99 ) +( G83 X1.000 Y2.000 Z0.000 F7380.000 Q1.500 R5.000 ) +G0 X1.000 Y2.000 +G1 Z5.000 F7380.000 +G1 Z3.500 F7380.000 +G0 Z5.000 +G0 Z3.575 +G1 Z2.000 F7380.000 +G0 Z5.000 +G0 Z2.075 +G1 Z0.500 F7380.000 +G0 Z5.000 +G0 Z0.575 +G1 Z0.000 F7380.000 +G0 Z5.000 +( G80 ) +G90 +(Finish operation: testpath) +(Begin postamble) +""" + args = "--comments --translate_drill" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test00270(self): + """Test translate_drill with G81 and G91.""" + # + c = Path.Command("G0 X1 Y2") + c1 = Path.Command("G0 Z8") + c2 = Path.Command("G91") + c3 = Path.Command("G99") + c4 = Path.Command("G81 X1 Y2 Z0 F123 R5") + c5 = Path.Command("G80") + c6 = Path.Command("G90") + + self.docobj.Path = Path.Path([c, c1, c2, c3, c4, c5, c6]) + postables = [self.docobj] + + expected = """G90 +G21 +G0 X1.000 Y2.000 +G0 Z8.000 +G91 +G99 +G81 X1.000 Y2.000 Z0.000 F7380.000 R5.000 +G80 +G90 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + expected = """G90 +G21 +G0 X1.000 Y2.000 +G0 Z8.000 +G91 +G90 +G0 Z13.000 +G0 X2.000 Y4.000 +G1 Z8.000 F7380.000 +G0 Z13.000 +G91 +G90 +""" + args = "--translate_drill" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + expected = """(Begin preamble) +G90 +G21 +(Begin operation) +G0 X1.000 Y2.000 +G0 Z8.000 +G91 +( G99 ) +( G81 X1.000 Y2.000 Z0.000 F7380.000 R5.000 ) +G90 +G0 Z13.000 +G0 X2.000 Y4.000 +G1 Z8.000 F7380.000 +G0 Z13.000 +G91 +( G80 ) +G90 +(Finish operation: testpath) +(Begin postamble) +""" + args = "--comments --translate_drill" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test00280(self): + """Test translate_drill with G82 and G91.""" + # + c = Path.Command("G0 X1 Y2") + c1 = Path.Command("G0 Z8") + c2 = Path.Command("G91") + c3 = Path.Command("G99") + c4 = Path.Command("G82 X1 Y2 Z0 F123 R5 P1.23456") + c5 = Path.Command("G80") + c6 = Path.Command("G90") + + self.docobj.Path = Path.Path([c, c1, c2, c3, c4, c5, c6]) + postables = [self.docobj] + + expected = """G90 +G21 +G0 X1.000 Y2.000 +G0 Z8.000 +G91 +G99 +G82 X1.000 Y2.000 Z0.000 F7380.000 R5.000 P1.23456 +G80 +G90 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + expected = """G90 +G21 +G0 X1.000 Y2.000 +G0 Z8.000 +G91 +G90 +G0 Z13.000 +G0 X2.000 Y4.000 +G1 Z8.000 F7380.000 +G4 P1.23456 +G0 Z13.000 +G91 +G90 +""" + args = "--translate_drill" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + expected = """(Begin preamble) +G90 +G21 +(Begin operation) +G0 X1.000 Y2.000 +G0 Z8.000 +G91 +( G99 ) +( G82 X1.000 Y2.000 Z0.000 F7380.000 R5.000 P1.23456 ) +G90 +G0 Z13.000 +G0 X2.000 Y4.000 +G1 Z8.000 F7380.000 +G4 P1.23456 +G0 Z13.000 +G91 +( G80 ) +G90 +(Finish operation: testpath) +(Begin postamble) +""" + args = "--comments --translate_drill" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test00290(self): + """Test translate_drill with G83 and G91.""" + # + c = Path.Command("G0 X1 Y2") + c1 = Path.Command("G0 Z8") + c2 = Path.Command("G91") + c3 = Path.Command("G99") + c4 = Path.Command("G83 X1 Y2 Z0 F123 Q1.5 R5") + c5 = Path.Command("G80") + c6 = Path.Command("G90") + + self.docobj.Path = Path.Path([c, c1, c2, c3, c4, c5, c6]) + postables = [self.docobj] + + expected = """G90 +G21 +G0 X1.000 Y2.000 +G0 Z8.000 +G91 +G99 +G83 X1.000 Y2.000 Z0.000 F7380.000 Q1.500 R5.000 +G80 +G90 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + expected = """G90 +G21 +G0 X1.000 Y2.000 +G0 Z8.000 +G91 +G90 +G0 Z13.000 +G0 X2.000 Y4.000 +G1 Z11.500 F7380.000 +G0 Z13.000 +G0 Z11.575 +G1 Z10.000 F7380.000 +G0 Z13.000 +G0 Z10.075 +G1 Z8.500 F7380.000 +G0 Z13.000 +G0 Z8.575 +G1 Z8.000 F7380.000 +G0 Z13.000 +G91 +G90 +""" + args = "--translate_drill" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + expected = """(Begin preamble) +G90 +G21 +(Begin operation) +G0 X1.000 Y2.000 +G0 Z8.000 +G91 +( G99 ) +( G83 X1.000 Y2.000 Z0.000 F7380.000 Q1.500 R5.000 ) +G90 +G0 Z13.000 +G0 X2.000 Y4.000 +G1 Z11.500 F7380.000 +G0 Z13.000 +G0 Z11.575 +G1 Z10.000 F7380.000 +G0 Z13.000 +G0 Z10.075 +G1 Z8.500 F7380.000 +G0 Z13.000 +G0 Z8.575 +G1 Z8.000 F7380.000 +G0 Z13.000 +G91 +( G80 ) +G90 +(Finish operation: testpath) +(Begin postamble) +""" + args = "--comments --translate_drill" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test00300(self): + """Test wait-for-spindle.""" + # + c = Path.Command("M3 S3000") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[2], "M3 S3000") + + args = "--wait-for-spindle=1.23456" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[2], "M3 S3000") + self.assertEqual(gcode.splitlines()[3], "G4 P1.23456") + + c = Path.Command("M4 S3000") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + # This also tests that the default for --wait-for-spindle + # goes back to 0.0 (no wait) + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[2], "M4 S3000") + + args = "--wait-for-spindle=1.23456" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode.splitlines()[2], "M4 S3000") + self.assertEqual(gcode.splitlines()[3], "G4 P1.23456") + + def test01000(self): + """Test G0 command Generation.""" + # + c = Path.Command("G0 X10 Y20 Z30 A40 B50 C60 U70 V80 W90") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G0 X10.000 Y20.000 Z30.000 A40.000 B50.000 C60.000 U70.000 V80.000 W90.000 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test01010(self): + """Test G1 command Generation.""" + # + c = Path.Command("G1 X10 Y20 Z30 A40 B50 C60 U70 V80 W90 F1.23456") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G1 X10.000 Y20.000 Z30.000 A40.000 B50.000 C60.000 U70.000 V80.000 W90.000 F74.074 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + # Test argument order + c = Path.Command("G1 F1.23456 Z30 V80 C60 W90 X10 B50 U70 Y20 A40") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G1 X10.000 Y20.000 Z30.000 A40.000 B50.000 C60.000 U70.000 V80.000 W90.000 F74.074 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test01020(self): + """Test G2 command Generation.""" + # + c = Path.Command("G2 X10 Y20 Z30 I40 J50 P60 F1.23456") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G2 X10.000 Y20.000 Z30.000 I40.000 J50.000 F74.074 P60 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + # + c = Path.Command("G2 X10 Y20 Z30 R40 P60 F1.23456") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G2 X10.000 Y20.000 Z30.000 F74.074 R40.000 P60 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test01030(self): + """Test G3 command Generation.""" + # + c = Path.Command("G3 X10 Y20 Z30 I40 J50 P60 F1.23456") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G3 X10.000 Y20.000 Z30.000 I40.000 J50.000 F74.074 P60 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + # + c = Path.Command("G3 X10 Y20 Z30 R40 P60 F1.23456") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G3 X10.000 Y20.000 Z30.000 F74.074 R40.000 P60 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test01040(self): + """Test G4 command Generation.""" + # Should some sort of "precision" be applied to the P parameter? + # The code as currently written does not do so intentionally. + # The P parameter indicates "time to wait" where a 0.001 would + # be a millisecond wait, so more than 3 or 4 digits of precision + # might be useful. + c = Path.Command("G4 P1.23456") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G4 P1.23456 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test01170(self): + """Test G17 command Generation.""" + # + c = Path.Command("G17") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G17 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test01171(self): + """Test G17.1 command Generation.""" + # + c = Path.Command("G17.1") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G17.1 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test01180(self): + """Test G18 command Generation.""" + # + c = Path.Command("G18") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G18 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test01181(self): + """Test G18.1 command Generation.""" + # + c = Path.Command("G18.1") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G18.1 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test01190(self): + """Test G19 command Generation.""" + # + c = Path.Command("G19") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G19 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test01191(self): + """Test G19.1 command Generation.""" + # + c = Path.Command("G19.1") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G19.1 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test01200(self): + """Test G20 command Generation.""" + # + c = Path.Command("G20") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G20 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test01210(self): + """Test G21 command Generation.""" + # + c = Path.Command("G21") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G21 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test01400(self): + """Test G40 command Generation.""" + # + c = Path.Command("G40") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G40 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test01410(self): + """Test G41 command Generation.""" + # + c = Path.Command("G41 D1.23456") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G41 D1 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + c = Path.Command("G41 D0") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G41 D0 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test01411(self): + """Test G41.1 command Generation.""" + # + c = Path.Command("G41.1 D1.23456 L3") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G41.1 L3 D1.23456 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test01420(self): + """Test G42 command Generation.""" + # + c = Path.Command("G42 D1.23456") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G42 D1 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + c = Path.Command("G42 D0") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G42 D0 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) + + def test01421(self): + """Test G42.1 command Generation.""" + # + c = Path.Command("G42.1 D1.23456 L3") + + self.docobj.Path = Path.Path([c]) + postables = [self.docobj] + + expected = """G90 +G21 +G42.1 L3 D1.23456 +""" + args = "" + gcode = postprocessor.export(postables, "gcode.tmp", args) + # print("--------\n" + gcode + "--------\n") + self.assertEqual(gcode, expected) diff --git a/src/Mod/Path/PathTests/boxtest1.fcstd b/src/Mod/Path/PathTests/boxtest1.fcstd new file mode 100644 index 0000000000..210307fd72 Binary files /dev/null and b/src/Mod/Path/PathTests/boxtest1.fcstd differ diff --git a/src/Mod/Path/PathTests/drill_test1.FCStd b/src/Mod/Path/PathTests/drill_test1.FCStd new file mode 100644 index 0000000000..b79b620edb Binary files /dev/null and b/src/Mod/Path/PathTests/drill_test1.FCStd differ diff --git a/src/Mod/Path/PathTests/test_linuxcnc_00.ngc b/src/Mod/Path/PathTests/test_linuxcnc_00.ngc deleted file mode 100644 index 6256f88731..0000000000 --- a/src/Mod/Path/PathTests/test_linuxcnc_00.ngc +++ /dev/null @@ -1,68 +0,0 @@ -G17 G90 -G21 -(Default_Tool) -M6 T2 -M3 S0.00 -(Contour) -(Uncompensated Tool Path) -G0 Z15.00 -G90 -G17 -G0 Z15.00 -G0 X10.00 Y10.00 -G0 Z10.00 -G1 X10.00 Y10.00 Z9.00 -G1 X10.00 Y0.00 Z9.00 -G1 X0.00 Y0.00 Z9.00 -G1 X0.00 Y10.00 Z9.00 -G1 X10.00 Y10.00 Z9.00 -G1 X10.00 Y10.00 Z8.00 -G1 X10.00 Y0.00 Z8.00 -G1 X0.00 Y0.00 Z8.00 -G1 X0.00 Y10.00 Z8.00 -G1 X10.00 Y10.00 Z8.00 -G1 X10.00 Y10.00 Z7.00 -G1 X10.00 Y0.00 Z7.00 -G1 X0.00 Y0.00 Z7.00 -G1 X0.00 Y10.00 Z7.00 -G1 X10.00 Y10.00 Z7.00 -G1 X10.00 Y10.00 Z6.00 -G1 X10.00 Y0.00 Z6.00 -G1 X0.00 Y0.00 Z6.00 -G1 X0.00 Y10.00 Z6.00 -G1 X10.00 Y10.00 Z6.00 -G1 X10.00 Y10.00 Z5.00 -G1 X10.00 Y0.00 Z5.00 -G1 X0.00 Y0.00 Z5.00 -G1 X0.00 Y10.00 Z5.00 -G1 X10.00 Y10.00 Z5.00 -G1 X10.00 Y10.00 Z4.00 -G1 X10.00 Y0.00 Z4.00 -G1 X0.00 Y0.00 Z4.00 -G1 X0.00 Y10.00 Z4.00 -G1 X10.00 Y10.00 Z4.00 -G1 X10.00 Y10.00 Z3.00 -G1 X10.00 Y0.00 Z3.00 -G1 X0.00 Y0.00 Z3.00 -G1 X0.00 Y10.00 Z3.00 -G1 X10.00 Y10.00 Z3.00 -G1 X10.00 Y10.00 Z2.00 -G1 X10.00 Y0.00 Z2.00 -G1 X0.00 Y0.00 Z2.00 -G1 X0.00 Y10.00 Z2.00 -G1 X10.00 Y10.00 Z2.00 -G1 X10.00 Y10.00 Z1.00 -G1 X10.00 Y0.00 Z1.00 -G1 X0.00 Y0.00 Z1.00 -G1 X0.00 Y10.00 Z1.00 -G1 X10.00 Y10.00 Z1.00 -G1 X10.00 Y10.00 Z0.00 -G1 X10.00 Y0.00 Z0.00 -G1 X0.00 Y0.00 Z0.00 -G1 X0.00 Y10.00 Z0.00 -G1 X10.00 Y10.00 Z0.00 -G0 Z15.00 -M05 -G00 X-1.0 Y1.0 -G17 G90 -M2 diff --git a/src/Mod/Path/TestPathApp.py b/src/Mod/Path/TestPathApp.py index e4d397a126..3d0a0606f9 100644 --- a/src/Mod/Path/TestPathApp.py +++ b/src/Mod/Path/TestPathApp.py @@ -38,6 +38,7 @@ from PathTests.TestPathHelixGenerator import TestPathHelixGenerator from PathTests.TestPathLog import TestPathLog from PathTests.TestPathOpTools import TestPathOpTools +# from PathTests.TestPathPost import TestPathPost from PathTests.TestPathPost import TestPathPostUtils from PathTests.TestPathPost import TestBuildPostList from PathTests.TestPathPost import TestOutputNameSubstitution @@ -58,11 +59,23 @@ from PathTests.TestPathUtil import TestPathUtil from PathTests.TestPathVcarve import TestPathVcarve from PathTests.TestPathVoronoi import TestPathVoronoi +from PathTests.TestCentroidPost import TestCentroidPost +from PathTests.TestGrblPost import TestGrblPost +from PathTests.TestLinuxCNCPost import TestLinuxCNCPost +from PathTests.TestMach3Mach4Post import TestMach3Mach4Post +from PathTests.TestRefactoredCentroidPost import TestRefactoredCentroidPost +from PathTests.TestRefactoredGrblPost import TestRefactoredGrblPost +from PathTests.TestRefactoredLinuxCNCPost import TestRefactoredLinuxCNCPost +from PathTests.TestRefactoredMach3Mach4Post import TestRefactoredMach3Mach4Post +from PathTests.TestRefactoredTestPost import TestRefactoredTestPost + # dummy usage to get flake8 and lgtm quiet False if depthTestCases.__name__ else True False if TestApp.__name__ else True +False if TestBuildPostList.__name__ else True False if TestDressupDogbone.__name__ else True False if TestHoldingTags.__name__ else True +False if TestOutputNameSubstitution.__name__ else True False if TestPathAdaptive.__name__ else True False if TestPathCore.__name__ else True False if TestPathDeburr.__name__ else True @@ -72,10 +85,7 @@ False if TestPathHelpers.__name__ else True # False if TestPathHelix.__name__ else True False if TestPathLog.__name__ else True False if TestPathOpTools.__name__ else True -# False if TestPathPostImport.__name__ else True # False if TestPathPost.__name__ else True -False if TestBuildPostList.__name__ else True -False if TestOutputNameSubstitution.__name__ else True False if TestPathPostUtils.__name__ else True False if TestPathPreferences.__name__ else True False if TestPathPropertyBag.__name__ else True @@ -94,3 +104,13 @@ False if TestPathVcarve.__name__ else True False if TestPathVoronoi.__name__ else True False if TestPathDrillGenerator.__name__ else True False if TestPathHelixGenerator.__name__ else True + +False if TestCentroidPost.__name__ else True +False if TestGrblPost.__name__ else True +False if TestLinuxCNCPost.__name__ else True +False if TestMach3Mach4Post.__name__ else True +False if TestRefactoredCentroidPost.__name__ else True +False if TestRefactoredGrblPost.__name__ else True +False if TestRefactoredLinuxCNCPost.__name__ else True +False if TestRefactoredMach3Mach4Post.__name__ else True +False if TestRefactoredTestPost.__name__ else True diff --git a/src/Mod/Sketcher/App/Sketch.cpp b/src/Mod/Sketcher/App/Sketch.cpp index c6a5e1f6e1..0ff1ff38a0 100644 --- a/src/Mod/Sketcher/App/Sketch.cpp +++ b/src/Mod/Sketcher/App/Sketch.cpp @@ -4043,7 +4043,7 @@ int Sketch::initBSplinePieceMove(int geoId, PointPos pos, const Base::Vector3d& GCS::BSpline &bsp = BSplines[Geoms[geoId].index]; // If spline has too few poles, just move all - if (bsp.poles.size() <= (bsp.degree + 1)) + if (bsp.poles.size() <= std::size_t(bsp.degree + 1)) return initMove(geoId, pos, fine); // Find the closest knot diff --git a/src/Mod/TechDraw/Gui/CommandCreateDims.cpp b/src/Mod/TechDraw/Gui/CommandCreateDims.cpp index e2474cffb1..e77e636e97 100644 --- a/src/Mod/TechDraw/Gui/CommandCreateDims.cpp +++ b/src/Mod/TechDraw/Gui/CommandCreateDims.cpp @@ -405,7 +405,9 @@ void CmdTechDrawDiameterDimension::activated(int iMsg) std::vector subs; int edgeType = _isValidSingleEdge(this); - if (edgeType == isEllipse) { + if (edgeType == isCircle) { + // nothing to do + } else if (edgeType == isEllipse) { QMessageBox::StandardButton result = QMessageBox::warning(Gui::getMainWindow(), QObject::tr("Ellipse Curve Warning"), QObject::tr("Selected edge is an Ellipse. Diameter will be approximate. Continue?"), @@ -426,7 +428,7 @@ void CmdTechDrawDiameterDimension::activated(int iMsg) QObject::tr("Selected edge is a BSpline and a diameter can not be calculated.")); return; } else { - QMessageBox::warning( + QMessageBox::warning( Gui::getMainWindow(), QObject::tr("Incorrect Selection"), QObject::tr("Selection for Diameter does not contain a circular edge " "(edge type: %1)") @@ -1479,7 +1481,7 @@ int _isValidSingleEdge(Gui::Command* cmd) { const std::vector SubNames = selection[0].getSubNames(); if (SubNames.size() != 1 || - TechDraw::DrawUtil::getGeomTypeFromName(SubNames[0]) == "Edge") { + TechDraw::DrawUtil::getGeomTypeFromName(SubNames[0]) != "Edge") { return isInvalid; } @@ -1560,9 +1562,9 @@ int _isValidEdgeToEdge(Gui::Command* cmd) { return isInvalid; } - //they both start with "Edge" - if(TechDraw::DrawUtil::getGeomTypeFromName(SubNames[0]) == "Edge" && - TechDraw::DrawUtil::getGeomTypeFromName(SubNames[1]) == "Edge") { + //they both must start with "Edge" + if(TechDraw::DrawUtil::getGeomTypeFromName(SubNames[0]) != "Edge" || + TechDraw::DrawUtil::getGeomTypeFromName(SubNames[1]) != "Edge") { return isInvalid; } @@ -1571,7 +1573,7 @@ int _isValidEdgeToEdge(Gui::Command* cmd) { TechDraw::BaseGeomPtr geom0 = objFeat0->getGeomByIndex(GeoId0); TechDraw::BaseGeomPtr geom1 = objFeat0->getGeomByIndex(GeoId1); - if ((!geom0) || (!geom1)) { // missing gometry + if ((geom0 == nullptr) || (geom1 == nullptr)) { // missing gometry Base::Console().Error("Logic Error: no geometry for GeoId: %d or GeoId: %d\n",GeoId0,GeoId1); return isInvalid; } @@ -1626,7 +1628,7 @@ bool _isValidVertexToEdge(Gui::Command* cmd) { } e = objFeat0->getGeomByIndex(eId); v = objFeat0->getProjVertexByIndex(vId); - if ((!e) || (!v)) { + if ((e == nullptr) || (v == nullptr)) { Base::Console().Error("Logic Error: no geometry for GeoId: %d or GeoId: %d\n",eId,vId); return false; }