diff --git a/src/Mod/Draft/Resources/Draft.qrc b/src/Mod/Draft/Resources/Draft.qrc
index c525de0d58..11e9289808 100644
--- a/src/Mod/Draft/Resources/Draft.qrc
+++ b/src/Mod/Draft/Resources/Draft.qrc
@@ -166,5 +166,6 @@
ui/TaskPanel_PolarArray.ui
ui/TaskSelectPlane.ui
ui/TaskShapeString.ui
+ ui/dialog_AnnotationStyleEditor.ui
diff --git a/src/Mod/Draft/Resources/ui/dialog_AnnotationStyleEditor.ui b/src/Mod/Draft/Resources/ui/dialog_AnnotationStyleEditor.ui
new file mode 100644
index 0000000000..eddf18cecc
--- /dev/null
+++ b/src/Mod/Draft/Resources/ui/dialog_AnnotationStyleEditor.ui
@@ -0,0 +1,447 @@
+
+
+ Dialog
+
+
+
+ 0
+ 0
+ 418
+ 694
+
+
+
+ Dialog
+
+
+ -
+
+
+ Style name
+
+
+
-
+
+
+ The name of your style. Existing style names can be edited
+
+
+ false
+
+
-
+
+
+
+
+ -
+
+ Add new...
+
+
+
+
+ -
+
+
+ false
+
+
+
+ 80
+ 16777215
+
+
+
+ Renames the selected style
+
+
+ Rename
+
+
+
+ -
+
+
+ false
+
+
+
+ 80
+ 16777215
+
+
+
+ Deletes the selected style
+
+
+ Delete
+
+
+
+
+
+
+ -
+
+
+ Text
+
+
+
-
+
+
+ Font size
+
+
+
+ -
+
+
+ Font name
+
+
+
+ -
+
+
+ Line spacing
+
+
+
+ -
+
+
+ The size of the text in real-world units
+
+
+
+
+
+
+ -
+
+
+ The spacing between lines of text in real-world units
+
+
+
+
+
+
+ -
+
+
+ The font to use for texts and dimensions
+
+
+
+
+
+
+ -
+
+
+ Units
+
+
+
-
+
+
+ Scale multiplier
+
+
+
+ -
+
+
+ Decimals
+
+
+
+ -
+
+
+ Únit override
+
+
+
+ -
+
+
+ Show unit
+
+
+
+ -
+
+
+ A multiplier value that affects distances shown by dimensions
+
+
+ 4
+
+
+ 1.000000000000000
+
+
+
+ -
+
+
+ Forces dimensions to be shown in a specific unit
+
+
+
+ -
+
+
+ The number of decimals to show on dimensions
+
+
+
+ -
+
+
+ Shows the units suffix on dimensions or not
+
+
+ Qt::RightToLeft
+
+
+
+
+
+
+
+
+
+ -
+
+
+ Line and arrows
+
+
+
-
+
+
+ Line width
+
+
+
+ -
+
+
+ Extension overshoot
+
+
+
+ -
+
+
+ Arrow size
+
+
+
+ -
+
+
+ Show lines
+
+
+
+ -
+
+
+ Dimension overshoot
+
+
+
+ -
+
+
+ Extension lines
+
+
+
+ -
+
+
+ Arrow type
+
+
+
+ -
+
+
+ Line / text color
+
+
+
+ -
+
+
+ Shows the dimension line or not
+
+
+ Qt::RightToLeft
+
+
+
+
+
+ true
+
+
+
+ -
+
+
+ The width of the dimension lines
+
+
+ px
+
+
+ 1
+
+
+
+ -
+
+
+ The color of dimension lines, arrows and texts
+
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ -
+
+
+ The typeof arrows to use for dimensions
+
+
-
+
+ Dot
+
+
+ -
+
+ Arrow
+
+
+ -
+
+ Tick
+
+
+
+
+ -
+
+
+ The size of dimension arrows
+
+
+
+
+
+
+ -
+
+
+ How far must the main dimension line extend pass the measured points
+
+
+
+
+
+
+ -
+
+
+ The length of extension lines
+
+
+
+
+
+
+ -
+
+
+ How far must the extension lines extend above the main dimension line
+
+
+
+
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+
+
+
+
+
+
+
+ Gui::QuantitySpinBox
+ QWidget
+
+
+
+ Gui::ColorButton
+ QPushButton
+
+
+
+
+
+
+ buttonBox
+ accepted()
+ Dialog
+ accept()
+
+
+ 248
+ 254
+
+
+ 157
+ 274
+
+
+
+
+ buttonBox
+ rejected()
+ Dialog
+ reject()
+
+
+ 316
+ 260
+
+
+ 286
+ 274
+
+
+
+
+
diff --git a/src/Mod/Draft/draftguitools/gui_annotationstyleeditor.py b/src/Mod/Draft/draftguitools/gui_annotationstyleeditor.py
new file mode 100644
index 0000000000..08c1dd698b
--- /dev/null
+++ b/src/Mod/Draft/draftguitools/gui_annotationstyleeditor.py
@@ -0,0 +1,252 @@
+# -*- coding: utf-8 -*-
+
+# ***************************************************************************
+# * Copyright (c) 2020 Yorik van Havre *
+# * *
+# * 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 *
+# * *
+# ***************************************************************************
+
+"""
+Provides all gui and tools to create and edit annotation styles
+Provides Draft_AnnotationStyleEditor command
+"""
+
+import FreeCAD,FreeCADGui
+import json
+
+EMPTYSTYLE = {
+ "FontName":"Sans",
+ "FontSize":0,
+ "LineSpacing":0,
+ "ScaleMultiplier":1,
+ "ShowUnit":False,
+ "UnitOverride":"",
+ "Decimals":0,
+ "ShowLines":True,
+ "LineWidth":1,
+ "LineColor":255,
+ "ArrowType":0,
+ "ArrowSize":0,
+ "DimensionOvershoot":0,
+ "ExtensionLines":0,
+ "ExtensionOvershoot":0,
+ }
+
+
+class Draft_AnnotationStyleEditor:
+
+ def __init__(self):
+
+ self.styles = {}
+
+ def GetResources(self):
+
+ return {'Pixmap' : ":icons/Draft_AnnotationStyleEditor.svg",
+ 'MenuText': QT_TRANSLATE_NOOP("Draft_AnnotationStyleEditor", "Annotation styles..."),
+ 'ToolTip' : QT_TRANSLATE_NOOP("Draft_AnnotationStyleEditor", "Manage or create annotation styles")}
+
+ def IsActive(self):
+
+ return bool(FreeCAD.ActiveDocument)
+
+ def Activated(self):
+
+ from PySide import QtGui
+
+ # load dialog
+ self.form = FreeCADGui.PySideUic.loadUi(":/ui/dialog_AnnotationStyleEditor.ui")
+
+ # center the dialog over FreeCAD window
+ mw = FreeCADGui.getMainWindow()
+ self.form.move(mw.frameGeometry().topLeft() + mw.rect().center() - self.form.rect().center())
+
+ # set icons
+ self.form.pushButtonDelete.setIcon(QtGui.QIcon(":/icons/edit_Cancel.svg"))
+ self.form.pushButtonRename.setIcon(QtGui.QIcon(":/icons/edit_Cancel.svg"))
+
+ # fill the styles combo
+ self.styles = self.read_meta()
+ for style in self.styles.keys():
+ self.form.comboBoxStyles.addItem(style)
+
+ # connect signals/slots
+ self.form.comboBoxStyles.currentIndexChanged.connect(self.on_style_changed)
+ self.form.pushButtonDelete.clicked.connect(self.on_delete)
+ self.form.pushButtonRename.clicked.connect(self.on_rename)
+ for attr in EMPTYSTYLE.keys():
+ control = getattr(self.form,attr)
+ for signal in ["textChanged","valueChanged","stateChanged"]:
+ if hasattr(control,signal):
+ getattr(control,signal).connect(self.update_style)
+ break
+
+ # show editor dialog
+ result = self.form.exec_()
+
+ # process if OK was clicked
+ if result:
+ self.save_meta(self.styles)
+
+ return
+
+ def read_meta(self):
+
+ """reads the document Meta property and returns a dict"""
+
+ styles = {}
+ meta = FreeCAD.ActiveDocument.Meta
+ for key,value in meta.items():
+ if key.startswith("Draft_Style_"):
+ styles[key[12:]] = json.loads(value)
+ return styles
+
+ def save_meta(self,styles):
+
+ """saves a dict to the document Meta property and updates objects"""
+
+ # save meta
+ changedstyles = []
+ meta = FreeCAD.ActiveDocument.Meta
+ for key,value in styles.items():
+ strvalue = json.dumps(value)
+ if meta["Draft_Style_"+key] and (meta["Draft_Style_"+key] != strvalue):
+ changedstyles.append(style)
+ meta["Draft_Style_"+key] = strvalue
+ FreeCAD.ActiveDocument.Meta = meta
+
+ # propagate changes to all annotations
+ for obj in self.get_annotations():
+ if obj.ViewObject.AnnotationStyle in styles.keys():
+ if obj.ViewObject.AnnotationStyle in changedstyles:
+ for attr,attrvalue in styles[obj.ViewObject.AnnotationStyle].items():
+ if hasattr(obj.ViewObject,attr):
+ setattr(obj.ViewObject,attr,attrvalue)
+ else:
+ obj.ViewObject.AnnotationStyle = " "
+ obj.ViewObject.AnnotationStyle == [" "] + styles.keys()
+
+ def on_style_changed(self,index):
+
+ """called when the styles combobox is changed"""
+
+ from PySide import QtGui
+
+ if index <= 1:
+ # nothing happens
+ self.form.pushButtonDelete.setEnabled(False)
+ self.form.pushButtonRename.setEnabled(False)
+ self.fill_editor(None)
+ if index == 1:
+ # Add new... entry
+ reply = QtGui.QInputDialog.getText(None, "Create new style","Style name:")
+ if reply[1]:
+ # OK or Enter pressed
+ name = reply[0]
+ if name in self.styles:
+ reply = QtGui.QMessageBox.information(None,"Style exists","This style name already exists")
+ else:
+ # create new default style
+ self.styles[name] = EMPTYSTYLE
+ self.form.comboBoxStyles.addItem(name)
+ self.form.comboBoxStyles.setCurrentIndex(self.form.comboBoxStyles.count()-1)
+ elif index > 1:
+ # Existing style
+ self.form.pushButtonDelete.setEnabled(True)
+ self.form.pushButtonRename.setEnabled(True)
+ self.fill_editor(self.form.comboBoxStyles.itemText(index))
+
+ def on_delete(self):
+
+ """called when the Delete button is pressed"""
+
+ from PySide import QtGui
+
+ index = self.form.comboBox.currentIndex()
+ style = self.form.comboBoxStyles.itemText(index)
+ if self.get_style_users(style):
+ reply = QtGui.QMessageBox.question(None, "Style in use", "This style is used by some objects in this document. Are you sure?",
+ QtGui.QMessageBox.Yes | QtGui.QMessageBox.No, QtGui.QMessageBox.No)
+ if reply == QtGui.QMessageBox.No:
+ return
+ self.form.comboBoxStyles.removeItem(index)
+ del self.styles[style]
+
+ def on_rename(self):
+
+ """called when the Rename button is pressed"""
+
+ from PySide import QtGui
+
+ index = self.form.comboBox.currentIndex()
+ style = self.form.comboBoxStyles.itemText(index)
+ reply = QtGui.QInputDialog.getText(None, "Rename style","New name:",QtGui.QLineEdit.Normal,style)
+ if reply[1]:
+ # OK or Enter pressed
+ newname = reply[0]
+ self.form.comboBoxStyles.setItemText(index,newname)
+ value = self.styles[style]
+ del self.styles[style]
+ self.styles[newname] = value
+
+ def fill_editor(self,style):
+
+ """fills the editor fields with the contents of a style"""
+
+ if style is None:
+ style = EMPTYSTYLE
+ for key,value in style.items():
+ setattr(self.form,key,value)
+
+ def update_style(self,arg=None):
+
+ """updates the current style with the values from the editor"""
+
+ index = self.form.comboBox.currentIndex()
+ if index > 1:
+ values = {}
+ style = self.form.comboBoxStyles.itemText(index)
+ for key in EMPTYSTYLE.keys():
+ control = getattr(self.form,key)
+ for attr in ["text","value","state"]:
+ if hasattr(control,attr):
+ values[key] = getattr(control,attr)
+ self.styles[style] = values
+
+ def get_annotations(self):
+
+ """gets all the objects that support annotation styles"""
+
+ users = []
+ for obj in FreeCAD.ActiveDocument.Objects:
+ vobj = obj.ViewObject
+ if hasattr(vobj,"AnnotationStyle"):
+ users.append(obj)
+ return users
+
+ def get_style_users(self,style):
+
+ """get all objects using a certain style"""
+
+ users = []
+ for obj in self.get_annotations():
+ if obj.ViewObject.AnnotationStyle == style:
+ users.append(obj)
+ return users
+
+
+FreeCADGui.addCommand('Draft_AnnotationStyleEditor', Draft_AnnotationStyleEditor())
diff --git a/src/Mod/Fem/CMakeLists.txt b/src/Mod/Fem/CMakeLists.txt
index 18f21804ca..304ee9d7e3 100755
--- a/src/Mod/Fem/CMakeLists.txt
+++ b/src/Mod/Fem/CMakeLists.txt
@@ -72,6 +72,7 @@ SET(FemInOut_SRCS
feminout/importCcxFrdResults.py
feminout/importFenicsMesh.py
feminout/importInpMesh.py
+ feminout/importPyMesh.py
feminout/importToolsFem.py
feminout/importVTKResults.py
feminout/importYamlJsonMesh.py
diff --git a/src/Mod/Fem/Init.py b/src/Mod/Fem/Init.py
index f12096a6fa..d06b24904f 100644
--- a/src/Mod/Fem/Init.py
+++ b/src/Mod/Fem/Init.py
@@ -27,6 +27,8 @@
import FreeCAD
+FreeCAD.addExportType("FEM mesh Python (*.meshpy)", "feminout.importPyMesh")
+
FreeCAD.addExportType("FEM mesh TetGen (*.poly)", "feminout.convert2TetGen")
# see FemMesh::read() and FemMesh::write() methods in src/Mod/Fem/App/FemMesh.cpp
diff --git a/src/Mod/Fem/feminout/importPyMesh.py b/src/Mod/Fem/feminout/importPyMesh.py
new file mode 100644
index 0000000000..7c64bec66a
--- /dev/null
+++ b/src/Mod/Fem/feminout/importPyMesh.py
@@ -0,0 +1,148 @@
+# ***************************************************************************
+# * Copyright (c) 2016 Bernd Hahnebach *
+# * *
+# * 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. *
+# * *
+# * 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 *
+# * *
+# ***************************************************************************
+
+__title__ = "FreeCAD Python Mesh reader and writer"
+__author__ = "Bernd Hahnebach"
+__url__ = "http://www.freecadweb.org"
+
+## @package importPyMesh
+# \ingroup FEM
+# \brief FreeCAD Python Mesh reader and writer for FEM workbench
+
+import FreeCAD
+from femmesh import meshtools
+
+# ************************************************************************************************
+# ********* generic FreeCAD import and export methods ********************************************
+# names are fix given from FreeCAD, these methods are called from FreeCAD
+# they are set in FEM modules Init.py
+
+if open.__module__ == "__builtin__":
+ # because we'll redefine open below (Python2)
+ pyopen = open
+elif open.__module__ == "io":
+ # because we'll redefine open below (Python3)
+ pyopen = open
+
+
+# export mesh to python
+def export(
+ objectslist,
+ filename
+):
+ "called when freecad exports a file"
+ if len(objectslist) != 1:
+ FreeCAD.Console.PrintError("This exporter can only export one object.\n")
+ return
+ obj = objectslist[0]
+ if not obj.isDerivedFrom("Fem::FemMeshObject"):
+ FreeCAD.Console.PrintError("No FEM mesh object selected.\n")
+ return
+ femnodes_mesh = obj.FemMesh.Nodes
+ femelement_table = meshtools.get_femelement_table(obj.FemMesh)
+ if meshtools.is_solid_femmesh(obj.FemMesh):
+ fem_mesh_type = "Solid"
+ elif meshtools.is_face_femmesh(obj.FemMesh):
+ fem_mesh_type = "Face"
+ elif meshtools.is_edge_femmesh(obj.FemMesh):
+ fem_mesh_type = "Edge"
+ else:
+ FreeCAD.Console.PrintError("Export of this FEM mesh to Python not supported.\n")
+ return
+ f = pyopen(filename, "w")
+ write_python_mesh_to_file(femnodes_mesh, femelement_table, fem_mesh_type, f)
+ f.close()
+
+
+# ************************************************************************************************
+# ********* module specific methods **************************************************************
+# writer:
+# - a method directly writes a FemMesh to the mesh file
+# - a method takes a file handle, mesh data and writes to the file handle
+
+# ********* writer *******************************************************************************
+
+def write(
+ fem_mesh,
+ filename
+):
+ """directly write a FemMesh to a Python mesh file
+ fem_mesh: a FemMesh"""
+
+ if not fem_mesh.isDerivedFrom("Fem::FemMesh"):
+ FreeCAD.Console.PrintError("Not a FemMesh was given as parameter.\n")
+ return
+ femnodes_mesh = fem_mesh.Nodes
+ femelement_table = meshtools.get_femelement_table(fem_mesh)
+ if meshtools.is_solid_femmesh(fem_mesh):
+ fem_mesh_type = "Solid"
+ elif meshtools.is_face_femmesh(fem_mesh):
+ fem_mesh_type = "Face"
+ elif meshtools.is_edge_femmesh(fem_mesh):
+ fem_mesh_type = "Edge"
+ else:
+ FreeCAD.Console.PrintError("Export of this FEM mesh to Python not supported.\n")
+ return
+ f = pyopen(filename, "w")
+ write_python_mesh_to_file(femnodes_mesh, femelement_table, fem_mesh_type, f)
+ f.close()
+
+
+def write_python_mesh_to_file(femnodes_mesh, femelement_table, fem_mesh_type, f):
+
+ mesh_name = "femmesh"
+
+ # nodes
+ f.write("def create_nodes(femmesh):\n")
+ f.write(" # nodes\n")
+ for node in femnodes_mesh:
+ # print(node, ' --> ', femnodes_mesh[node])
+ vec = femnodes_mesh[node]
+ f.write(
+ " {0}.addNode({1}, {2}, {3}, {4})\n"
+ .format(mesh_name, vec.x, vec.y, vec.z, node)
+ )
+ f.write(" return True\n")
+ f.write("\n\n")
+
+ # elements
+ f.write("def create_elements(femmesh):\n")
+ f.write(" # elements\n")
+ for element in femelement_table:
+ # print(element, ' --> ', femelement_table[element])
+ if fem_mesh_type == "Solid":
+ f.write(
+ " {0}.addVolume({1}, {2})\n"
+ .format(mesh_name, list(femelement_table[element]), element)
+ )
+ elif fem_mesh_type == "Face":
+ f.write(
+ " {0}.addFace({1}, {2})\n"
+ .format(mesh_name, list(femelement_table[element]), element)
+ )
+ elif fem_mesh_type == "Edge":
+ f.write(
+ " {0}.addEdge({1}, {2})\n"
+ .format(mesh_name, list(femelement_table[element]), element)
+ )
+ f.write(" return True\n")
diff --git a/src/Mod/Fem/femmesh/meshtools.py b/src/Mod/Fem/femmesh/meshtools.py
index 97b353baec..8c2241d7f8 100644
--- a/src/Mod/Fem/femmesh/meshtools.py
+++ b/src/Mod/Fem/femmesh/meshtools.py
@@ -1699,17 +1699,17 @@ def get_contact_obj_faces(
"(example: multiple element faces per master or slave\n"
)
- FreeCAD.Console.PrintLog("Slave: {}, {}\n".format(slave_ref[0].Name, slave_ref))
- FreeCAD.Console.PrintLog("Master: {}, {}\n".format(master_ref[0].Name, master_ref))
+ FreeCAD.Console.PrintLog(" Slave: {}, {}\n".format(slave_ref[0].Name, slave_ref))
+ FreeCAD.Console.PrintLog(" Master: {}, {}\n".format(master_ref[0].Name, master_ref))
if is_solid_femmesh(femmesh):
- # get the nodes, sorted and duplicates removed
+ FreeCAD.Console.PrintLog(" Get the nodes, sorted and duplicates removed.\n")
slaveface_nds = sorted(list(set(get_femnodes_by_refshape(femmesh, slave_ref))))
masterface_nds = sorted(list(set(get_femnodes_by_refshape(femmesh, master_ref))))
- # FreeCAD.Console.PrintLog("slaveface_nds: {}\n".format(slaveface_nds))
- # FreeCAD.Console.PrintLog("masterface_nds: {}\n".format(slaveface_nds))
+ FreeCAD.Console.PrintLog(" slaveface_nds: {}\n".format(slaveface_nds))
+ FreeCAD.Console.PrintLog(" masterface_nds: {}\n".format(slaveface_nds))
- # fill the bit_pattern_dict and search for the faces
+ FreeCAD.Console.PrintLog(" Fill the bit_pattern_dict and search for the faces.\n")
slave_bit_pattern_dict = get_bit_pattern_dict(
femelement_table,
femnodes_ele_table,
@@ -1721,16 +1721,18 @@ def get_contact_obj_faces(
masterface_nds
)
- # get the faces ids
+ FreeCAD.Console.PrintLog(" Get the FaceIDs.\n")
slave_faces = get_ccxelement_faces_from_binary_search(slave_bit_pattern_dict)
master_faces = get_ccxelement_faces_from_binary_search(master_bit_pattern_dict)
elif is_face_femmesh(femmesh):
slave_ref_shape = slave_ref[0].Shape.getElement(slave_ref[1][0])
master_ref_shape = master_ref[0].Shape.getElement(master_ref[1][0])
- # get the faces ids
+
+ FreeCAD.Console.PrintLog(" Get the FaceIDs.\n")
slave_face_ids = femmesh.getFacesByFace(slave_ref_shape)
master_face_ids = femmesh.getFacesByFace(master_ref_shape)
+
# build slave_faces and master_faces
# face 2 for tria6 element
# is it face 2 for all shell elements
@@ -1739,8 +1741,13 @@ def get_contact_obj_faces(
for fid in master_face_ids:
master_faces.append([fid, 2])
- FreeCAD.Console.PrintLog("slave_faces: {}\n".format(slave_faces))
- FreeCAD.Console.PrintLog("master_faces: {}\n".format(master_faces))
+ FreeCAD.Console.PrintLog(" Master and slave face ready to use for writer:\n")
+ FreeCAD.Console.PrintLog(" slave_faces: {}\n".format(slave_faces))
+ FreeCAD.Console.PrintLog(" master_faces: {}\n".format(master_faces))
+ if len(slave_faces) == 0:
+ FreeCAD.Console.PrintError("No faces found for contact slave face.\n")
+ if len(master_faces) == 0:
+ FreeCAD.Console.PrintError("No faces found for contact master face.\n")
return [slave_faces, master_faces]
diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt
index ded7c91a93..78415e1e32 100644
--- a/src/Mod/Path/CMakeLists.txt
+++ b/src/Mod/Path/CMakeLists.txt
@@ -107,6 +107,7 @@ SET(PathScripts_SRCS
PathScripts/PathStop.py
PathScripts/PathSurface.py
PathScripts/PathSurfaceGui.py
+ PathScripts/PathSurfaceSupport.py
PathScripts/PathToolBit.py
PathScripts/PathToolBitCmd.py
PathScripts/PathToolBitEdit.py
diff --git a/src/Mod/Path/Gui/Resources/panels/PageOpSurfaceEdit.ui b/src/Mod/Path/Gui/Resources/panels/PageOpSurfaceEdit.ui
index e4aaf19e5e..bb14461cc4 100644
--- a/src/Mod/Path/Gui/Resources/panels/PageOpSurfaceEdit.ui
+++ b/src/Mod/Path/Gui/Resources/panels/PageOpSurfaceEdit.ui
@@ -6,7 +6,7 @@
0
0
- 350
+ 368
400
@@ -59,6 +59,9 @@
-
+
+ <html><head/><body><p>Planar: Flat, 3D surface scan. Rotational: 4th-axis rotational scan.</p></body></html>
+
-
Planar
@@ -73,6 +76,9 @@
-
+
+ <html><head/><body><p>Complete the operation in a single pass at depth, or mulitiple passes to final depth.</p></body></html>
+
-
Single-pass
@@ -127,6 +133,9 @@
-
+
+ <html><head/><body><p>Enable optimization of linear paths (co-linear points). Removes unnecessary co-linear points from G-Code output.</p></body></html>
+
Optimize Linear Paths
@@ -148,6 +157,9 @@
-
+
+ <html><head/><body><p>Make True, if specifying a Start Point</p></body></html>
+
Use Start Point
@@ -169,6 +181,9 @@
-
+
+ <html><head/><body><p>Set the Z-axis depth offset from the target surface.</p></body></html>
+
mm
@@ -184,6 +199,9 @@
0
+
+ <html><head/><body><p>Additional offset to the selected bounding box along the X axis."</p></body></html>
+
mm
@@ -191,6 +209,9 @@
-
+
+ <html><head/><body><p>Additional offset to the selected bounding box along the Y axis."</p></body></html>
+
mm
@@ -200,6 +221,9 @@
-
+
+ <html><head/><body><p>Set the sampling resolution. Smaller values quickly increase processing time.</p></body></html>
+
mm
@@ -214,6 +238,9 @@
-
+
+ <html><head/><body><p>Dropcutter lines are created parallel to this axis.</p></body></html>
+
-
X
@@ -228,6 +255,9 @@
-
+
+ <html><head/><body><p>Select the overall boundary for the operation.</p></body></html>
+
-
Stock
@@ -242,6 +272,9 @@
-
+
+ <html><head/><body><p>Enable separate optimization of transitions between, and breaks within, each step over path.</p></body></html>
+
Optimize StepOver Transitions
@@ -256,16 +289,9 @@
-
-
-
-
- Line
-
-
- -
-
- ZigZag
-
-
+
+ <html><head/><body><p>Set the geometric clearing pattern to use for the operation.</p></body></html>
+
-
Circular
@@ -276,6 +302,26 @@
CircularZigZag
+ -
+
+ Line
+
+
+ -
+
+ Offset
+
+
+ -
+
+ Spiral
+
+
+ -
+
+ ZigZag
+
+
@@ -299,8 +345,8 @@
Gui::InputField
- QWidget
-
+ QLineEdit
+
diff --git a/src/Mod/Path/Gui/Resources/panels/PageOpWaterlineEdit.ui b/src/Mod/Path/Gui/Resources/panels/PageOpWaterlineEdit.ui
index 5e0edef1c9..82533fe061 100644
--- a/src/Mod/Path/Gui/Resources/panels/PageOpWaterlineEdit.ui
+++ b/src/Mod/Path/Gui/Resources/panels/PageOpWaterlineEdit.ui
@@ -50,6 +50,9 @@
8
+
+ <html><head/><body><p>Select the overall boundary for the operation.</p></body></html>
+
-
Stock
@@ -70,6 +73,9 @@
0
+
+ <html><head/><body><p>Positive values push the cutter toward, or beyond, the boundary. Negative values retract the cutter away from the boundary.</p></body></html>
+
-
@@ -79,6 +85,9 @@
8
+
+ <html><head/><body><p>Complete the operation in a single pass at depth, or mulitiple passes to final depth.</p></body></html>
+
-
Single-pass
@@ -93,6 +102,9 @@
-
+
+ <html><head/><body><p>Select the algorithm to use: OCL Dropcutter*, or Experimental (Not OCL based).</p></body></html>
+
-
OCL Dropcutter
@@ -107,6 +119,9 @@
-
+
+ <html><head/><body><p>Enable optimization of linear paths (co-linear points). Removes unnecessary co-linear points from G-Code output.</p></body></html>
+
Optimize Linear Paths
@@ -132,6 +147,9 @@
8
+
+ <html><head/><body><p>Set the geometric clearing pattern to use for the operation.</p></body></html>
+
-
None
@@ -213,6 +231,9 @@
-
+
+ <html><head/><body><p>Set the sampling resolution. Smaller values quickly increase processing time.</p></body></html>
+
mm
@@ -260,8 +281,8 @@
Gui::InputField
- QWidget
-
+ QLineEdit
+
diff --git a/src/Mod/Path/PathScripts/PathSurface.py b/src/Mod/Path/PathScripts/PathSurface.py
index 40cd771398..9c6f2d3c0c 100644
--- a/src/Mod/Path/PathScripts/PathSurface.py
+++ b/src/Mod/Path/PathScripts/PathSurface.py
@@ -1,3744 +1,2189 @@
-# -*- coding: utf-8 -*-
-
-# ***************************************************************************
-# * *
-# * Copyright (c) 2016 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 *
-# * *
-# ***************************************************************************
-# * *
-# * Additional modifications and contributions beginning 2019 *
-# * by Russell Johnson 2020-03-18 12:29 CST *
-# * *
-# ***************************************************************************
-
-from __future__ import print_function
-
-import FreeCAD
-import Path
-import PathScripts.PathLog as PathLog
-import PathScripts.PathUtils as PathUtils
-import PathScripts.PathOp as PathOp
-
-from PySide import QtCore
-import time
-import math
-
-# lazily loaded modules
-from lazy_loader.lazy_loader import LazyLoader
-MeshPart = LazyLoader('MeshPart', globals(), 'MeshPart')
-Draft = LazyLoader('Draft', globals(), 'Draft')
-Part = LazyLoader('Part', globals(), 'Part')
-
-if FreeCAD.GuiUp:
- import FreeCADGui
-
-__title__ = "Path Surface Operation"
-__author__ = "sliptonic (Brad Collette)"
-__url__ = "http://www.freecadweb.org"
-__doc__ = "Class and implementation of Mill Facing operation."
-
-PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
-# PathLog.trackModule(PathLog.thisModule())
-
-
-# Qt translation handling
-def translate(context, text, disambig=None):
- return QtCore.QCoreApplication.translate(context, text, disambig)
-
-
-# OCL must be installed
-try:
- import ocl
-except ImportError:
- FreeCAD.Console.PrintError(
- translate("Path_Surface", "This operation requires OpenCamLib to be installed.") + "\n")
- import sys
- sys.exit(translate("Path_Surface", "This operation requires OpenCamLib to be installed."))
-
-
-class ObjectSurface(PathOp.ObjectOp):
- '''Proxy object for Surfacing operation.'''
-
- def baseObject(self):
- '''baseObject() ... returns super of receiver
- Used to call base implementation in overwritten functions.'''
- return super(self.__class__, self)
-
- def opFeatures(self, obj):
- '''opFeatures(obj) ... return all standard features and edges based geomtries'''
- return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureHeights | PathOp.FeatureStepDown | PathOp.FeatureCoolant | PathOp.FeatureBaseFaces
-
- def initOperation(self, obj):
- '''initPocketOp(obj) ... create operation specific properties'''
- self.initOpProperties(obj)
-
- # For debugging
- if PathLog.getLevel(PathLog.thisModule()) != 4:
- obj.setEditorMode('ShowTempObjects', 2) # hide
-
- if not hasattr(obj, 'DoNotSetDefaultValues'):
- self.setEditorProperties(obj)
-
- def initOpProperties(self, obj):
- '''initOpProperties(obj) ... create operation specific properties'''
-
- PROPS = [
- ("App::PropertyBool", "ShowTempObjects", "Debug",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Show the temporary path construction objects when module is in DEBUG mode.")),
-
- ("App::PropertyDistance", "AngularDeflection", "Mesh Conversion",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate mesh. Smaller values increase processing time a lot.")),
- ("App::PropertyDistance", "LinearDeflection", "Mesh Conversion",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate mesh. Smaller values do not increase processing time much.")),
-
- ("App::PropertyFloat", "CutterTilt", "Rotational",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Stop index(angle) for rotational scan")),
- ("App::PropertyEnumeration", "DropCutterDir", "Rotational",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "The direction along which dropcutter lines are created")),
- ("App::PropertyVectorDistance", "DropCutterExtraOffset", "Rotational",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Additional offset to the selected bounding box")),
- ("App::PropertyEnumeration", "RotationAxis", "Rotational",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "The model will be rotated around this axis.")),
- ("App::PropertyFloat", "StartIndex", "Rotational",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Start index(angle) for rotational scan")),
- ("App::PropertyFloat", "StopIndex", "Rotational",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Stop index(angle) for rotational scan")),
-
- ("App::PropertyEnumeration", "ScanType", "Surface",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Planar: Flat, 3D surface scan. Rotational: 4th-axis rotational scan.")),
-
- ("App::PropertyInteger", "AvoidLastX_Faces", "Selected Geometry Settings",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Avoid cutting the last 'N' faces in the Base Geometry list of selected faces.")),
- ("App::PropertyBool", "AvoidLastX_InternalFeatures", "Selected Geometry Settings",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Do not cut internal features on avoided faces.")),
- ("App::PropertyDistance", "BoundaryAdjustment", "Selected Geometry Settings",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Positive values push the cutter toward, or beyond, the boundary. Negative values retract the cutter away from the boundary.")),
- ("App::PropertyBool", "BoundaryEnforcement", "Selected Geometry Settings",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "If true, the cutter will remain inside the boundaries of the model or selected face(s).")),
- ("App::PropertyEnumeration", "HandleMultipleFeatures", "Selected Geometry Settings",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose how to process multiple Base Geometry features.")),
- ("App::PropertyDistance", "InternalFeaturesAdjustment", "Selected Geometry Settings",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Positive values push the cutter toward, or into, the feature. Negative values retract the cutter away from the feature.")),
- ("App::PropertyBool", "InternalFeaturesCut", "Selected Geometry Settings",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Ignore internal feature areas within a larger selected face.")),
-
- ("App::PropertyEnumeration", "BoundBox", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Select the overall boundary for the operation. ")),
- ("App::PropertyVectorDistance", "CircularCenterCustom", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the start point for circular cut patterns.")),
- ("App::PropertyEnumeration", "CircularCenterAt", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose location of the center point for starting the circular pattern.")),
- ("App::PropertyEnumeration", "CutMode", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the direction for the cutting tool to engage the material: Climb (ClockWise) or Conventional (CounterClockWise)")),
- ("App::PropertyEnumeration", "CutPattern", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the geometric clearing pattern to use for the operation.")),
- ("App::PropertyFloat", "CutPatternAngle", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "The yaw angle used for certain clearing patterns")),
- ("App::PropertyBool", "CutPatternReversed", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Reverse the cut order of the stepover paths. For circular cut patterns, begin at the outside and work toward the center.")),
- ("App::PropertyDistance", "DepthOffset", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the Z-axis depth offset from the target surface.")),
- ("App::PropertyEnumeration", "LayerMode", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Complete the operation in a single pass at depth, or mulitiple passes to final depth.")),
- ("App::PropertyEnumeration", "ProfileEdges", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Profile the edges of the selection.")),
- ("App::PropertyDistance", "SampleInterval", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the sampling resolution. Smaller values quickly increase processing time.")),
- ("App::PropertyPercent", "StepOver", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the stepover percentage, based on the tool's diameter.")),
-
- ("App::PropertyBool", "OptimizeLinearPaths", "Optimization",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable optimization of linear paths (co-linear points). Removes unnecessary co-linear points from G-Code output.")),
- ("App::PropertyBool", "OptimizeStepOverTransitions", "Optimization",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable separate optimization of transitions between, and breaks within, each step over path.")),
- ("App::PropertyBool", "CircularUseG2G3", "Optimization",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Convert co-planar arcs to G2/G3 gcode commands for `Circular` and `CircularZigZag` cut patterns.")),
- ("App::PropertyDistance", "GapThreshold", "Optimization",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Collinear and co-radial artifact gaps that are smaller than this threshold are closed in the path.")),
- ("App::PropertyString", "GapSizes", "Optimization",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Feedback: three smallest gaps identified in the path geometry.")),
-
- ("App::PropertyVectorDistance", "StartPoint", "Start Point",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "The custom start point for the path of this operation")),
- ("App::PropertyBool", "UseStartPoint", "Start Point",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Make True, if specifying a Start Point"))
- ]
-
- missing = list()
- for (prtyp, nm, grp, tt) in PROPS:
- if not hasattr(obj, nm):
- obj.addProperty(prtyp, nm, grp, tt)
- missing.append(nm)
-
- # Set enumeration lists for enumeration properties
- if len(missing) > 0:
- ENUMS = self._propertyEnumerations()
- for n in ENUMS:
- if n in missing:
- cmdStr = 'obj.{}={}'.format(n, ENUMS[n])
- exec(cmdStr)
-
- self.addedAllProperties = True
-
- def _propertyEnumerations(self):
- # Enumeration lists for App::PropertyEnumeration properties
- return {
- 'BoundBox': ['BaseBoundBox', 'Stock'],
- 'CircularCenterAt': ['CenterOfMass', 'CenterOfBoundBox', 'XminYmin', 'Custom'],
- 'CutMode': ['Conventional', 'Climb'],
- 'CutPattern': ['Line', 'Circular', 'CircularZigZag', 'ZigZag'], # Additional goals ['Offset', 'Spiral', 'ZigZagOffset', 'Grid', 'Triangle']
- 'DropCutterDir': ['X', 'Y'],
- 'HandleMultipleFeatures': ['Collectively', 'Individually'],
- 'LayerMode': ['Single-pass', 'Multi-pass'],
- 'ProfileEdges': ['None', 'Only', 'First', 'Last'],
- 'RotationAxis': ['X', 'Y'],
- 'ScanType': ['Planar', 'Rotational']
- }
-
- def setEditorProperties(self, obj):
- # Used to hide inputs in properties list
-
- mode = 2 # 2=hidden
- if obj.ScanType == 'Planar':
- show = 0
- hide = 2
- # if obj.CutPattern in ['Line', 'ZigZag']:
- if obj.CutPattern in ['Circular', 'CircularZigZag']:
- show = 2 # hide
- hide = 0 # show
- obj.setEditorMode('CutPatternAngle', show)
- obj.setEditorMode('CircularCenterAt', hide)
- obj.setEditorMode('CircularCenterCustom', hide)
- elif obj.ScanType == 'Rotational':
- mode = 0 # show and editable
- obj.setEditorMode('DropCutterDir', mode)
- obj.setEditorMode('DropCutterExtraOffset', mode)
- obj.setEditorMode('RotationAxis', mode)
- obj.setEditorMode('StartIndex', mode)
- obj.setEditorMode('StopIndex', mode)
- obj.setEditorMode('CutterTilt', mode)
-
- def onChanged(self, obj, prop):
- if hasattr(self, 'addedAllProperties'):
- if self.addedAllProperties is True:
- if prop == 'ScanType':
- self.setEditorProperties(obj)
- if prop == 'CutPattern':
- self.setEditorProperties(obj)
-
- def opOnDocumentRestored(self, obj):
- self.initOpProperties(obj)
-
- if PathLog.getLevel(PathLog.thisModule()) != 4:
- obj.setEditorMode('ShowTempObjects', 2) # hide
- else:
- obj.setEditorMode('ShowTempObjects', 0) # show
-
- self.setEditorProperties(obj)
-
- def opSetDefaultValues(self, obj, job):
- '''opSetDefaultValues(obj, job) ... initialize defaults'''
- job = PathUtils.findParentJob(obj)
-
- obj.OptimizeLinearPaths = True
- obj.InternalFeaturesCut = True
- obj.OptimizeStepOverTransitions = False
- obj.CircularUseG2G3 = False
- obj.BoundaryEnforcement = True
- obj.UseStartPoint = False
- obj.AvoidLastX_InternalFeatures = True
- obj.CutPatternReversed = False
- obj.StartPoint.x = 0.0
- obj.StartPoint.y = 0.0
- obj.StartPoint.z = obj.ClearanceHeight.Value
- obj.ProfileEdges = 'None'
- obj.LayerMode = 'Single-pass'
- obj.ScanType = 'Planar'
- obj.RotationAxis = 'X'
- obj.CutMode = 'Conventional'
- obj.CutPattern = 'Line'
- obj.HandleMultipleFeatures = 'Collectively' # 'Individually'
- obj.CircularCenterAt = 'CenterOfMass' # 'CenterOfBoundBox', 'XminYmin', 'Custom'
- obj.GapSizes = 'No gaps identified.'
- obj.StepOver = 100
- obj.CutPatternAngle = 0.0
- obj.CutterTilt = 0.0
- obj.StartIndex = 0.0
- obj.StopIndex = 360.0
- obj.SampleInterval.Value = 1.0
- obj.BoundaryAdjustment.Value = 0.0
- obj.InternalFeaturesAdjustment.Value = 0.0
- obj.AvoidLastX_Faces = 0
- obj.CircularCenterCustom.x = 0.0
- obj.CircularCenterCustom.y = 0.0
- obj.CircularCenterCustom.z = 0.0
- obj.GapThreshold.Value = 0.005
- obj.AngularDeflection.Value = 0.25
- obj.LinearDeflection.Value = job.GeometryTolerance
- # For debugging
- obj.ShowTempObjects = False
-
- # need to overwrite the default depth calculations for facing
- d = None
- if job:
- if job.Stock:
- d = PathUtils.guessDepths(job.Stock.Shape, None)
- PathLog.debug("job.Stock exists")
- else:
- PathLog.debug("job.Stock NOT exist")
- else:
- PathLog.debug("job NOT exist")
-
- if d is not None:
- obj.OpFinalDepth.Value = d.final_depth
- obj.OpStartDepth.Value = d.start_depth
- else:
- obj.OpFinalDepth.Value = -10
- obj.OpStartDepth.Value = 10
-
- PathLog.debug('Default OpFinalDepth: {}'.format(obj.OpFinalDepth.Value))
- PathLog.debug('Defualt OpStartDepth: {}'.format(obj.OpStartDepth.Value))
-
- def opApplyPropertyLimits(self, obj):
- '''opApplyPropertyLimits(obj) ... Apply necessary limits to user input property values before performing main operation.'''
- # Limit start index
- if obj.StartIndex < 0.0:
- obj.StartIndex = 0.0
- if obj.StartIndex > 360.0:
- obj.StartIndex = 360.0
-
- # Limit stop index
- if obj.StopIndex > 360.0:
- obj.StopIndex = 360.0
- if obj.StopIndex < 0.0:
- obj.StopIndex = 0.0
-
- # Limit cutter tilt
- if obj.CutterTilt < -90.0:
- obj.CutterTilt = -90.0
- if obj.CutterTilt > 90.0:
- obj.CutterTilt = 90.0
-
- # Limit sample interval
- if obj.SampleInterval.Value < 0.0001:
- obj.SampleInterval.Value = 0.0001
- PathLog.error(translate('PathSurface', 'Sample interval limits are 0.001 to 25.4 millimeters.'))
- if obj.SampleInterval.Value > 25.4:
- obj.SampleInterval.Value = 25.4
- PathLog.error(translate('PathSurface', 'Sample interval limits are 0.001 to 25.4 millimeters.'))
-
- # Limit cut pattern angle
- if obj.CutPatternAngle < -360.0:
- obj.CutPatternAngle = 0.0
- PathLog.error(translate('PathSurface', 'Cut pattern angle limits are +-360 degrees.'))
- if obj.CutPatternAngle >= 360.0:
- obj.CutPatternAngle = 0.0
- PathLog.error(translate('PathSurface', 'Cut pattern angle limits are +- 360 degrees.'))
-
- # Limit StepOver to natural number percentage
- if obj.StepOver > 100:
- obj.StepOver = 100
- if obj.StepOver < 1:
- obj.StepOver = 1
-
- # Limit AvoidLastX_Faces to zero and positive values
- if obj.AvoidLastX_Faces < 0:
- obj.AvoidLastX_Faces = 0
- PathLog.error(translate('PathSurface', 'AvoidLastX_Faces: Only zero or positive values permitted.'))
- if obj.AvoidLastX_Faces > 100:
- obj.AvoidLastX_Faces = 100
- PathLog.error(translate('PathSurface', 'AvoidLastX_Faces: Avoid last X faces count limited to 100.'))
-
- def opExecute(self, obj):
- '''opExecute(obj) ... process surface operation'''
- PathLog.track()
-
- self.modelSTLs = list()
- self.safeSTLs = list()
- self.modelTypes = list()
- self.boundBoxes = list()
- self.profileShapes = list()
- self.collectiveShapes = list()
- self.individualShapes = list()
- self.avoidShapes = list()
- self.deflection = None
- self.tempGroup = None
- self.CutClimb = False
- self.closedGap = False
- self.gaps = [0.1, 0.2, 0.3]
- CMDS = list()
- modelVisibility = list()
- FCAD = FreeCAD.ActiveDocument
-
- # Set debugging behavior
- self.showDebugObjects = False # Set to true if you want a visual DocObjects created for some path construction objects
- self.showDebugObjects = obj.ShowTempObjects
- deleteTempsFlag = True # Set to False for debugging
- if PathLog.getLevel(PathLog.thisModule()) == 4:
- deleteTempsFlag = False
- else:
- self.showDebugObjects = False
-
- # mark beginning of operation and identify parent Job
- PathLog.info('\nBegin 3D Surface operation...')
- startTime = time.time()
-
- # Identify parent Job
- JOB = PathUtils.findParentJob(obj)
- if JOB is None:
- PathLog.error(translate('PathSurface', "No JOB"))
- return
- self.stockZMin = JOB.Stock.Shape.BoundBox.ZMin
-
- # set cut mode; reverse as needed
- if obj.CutMode == 'Climb':
- self.CutClimb = True
- if obj.CutPatternReversed is True:
- if self.CutClimb is True:
- self.CutClimb = False
- else:
- self.CutClimb = True
-
- # Begin GCode for operation with basic information
- # ... and move cutter to clearance height and startpoint
- output = ''
- if obj.Comment != '':
- self.commandlist.append(Path.Command('N ({})'.format(str(obj.Comment)), {}))
- self.commandlist.append(Path.Command('N ({})'.format(obj.Label), {}))
- self.commandlist.append(Path.Command('N (Tool type: {})'.format(str(obj.ToolController.Tool.ToolType)), {}))
- self.commandlist.append(Path.Command('N (Compensated Tool Path. Diameter: {})'.format(str(obj.ToolController.Tool.Diameter)), {}))
- self.commandlist.append(Path.Command('N (Sample interval: {})'.format(str(obj.SampleInterval.Value)), {}))
- self.commandlist.append(Path.Command('N (Step over %: {})'.format(str(obj.StepOver)), {}))
- self.commandlist.append(Path.Command('N ({})'.format(output), {}))
- self.commandlist.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid}))
- if obj.UseStartPoint is True:
- self.commandlist.append(Path.Command('G0', {'X': obj.StartPoint.x, 'Y': obj.StartPoint.y, 'F': self.horizRapid}))
-
- # Instantiate additional class operation variables
- self.resetOpVariables()
-
- # Impose property limits
- self.opApplyPropertyLimits(obj)
-
- # Create temporary group for temporary objects, removing existing
- # if self.showDebugObjects is True:
- tempGroupName = 'tempPathSurfaceGroup'
- if FCAD.getObject(tempGroupName):
- for to in FCAD.getObject(tempGroupName).Group:
- FCAD.removeObject(to.Name)
- FCAD.removeObject(tempGroupName) # remove temp directory if already exists
- if FCAD.getObject(tempGroupName + '001'):
- for to in FCAD.getObject(tempGroupName + '001').Group:
- FCAD.removeObject(to.Name)
- FCAD.removeObject(tempGroupName + '001') # remove temp directory if already exists
- tempGroup = FCAD.addObject('App::DocumentObjectGroup', tempGroupName)
- tempGroupName = tempGroup.Name
- self.tempGroup = tempGroup
- tempGroup.purgeTouched()
- # Add temp object to temp group folder with following code:
- # ... self.tempGroup.addObject(OBJ)
-
- # Setup cutter for OCL and cutout value for operation - based on tool controller properties
- self.cutter = self.setOclCutter(obj)
- self.safeCutter = self.setOclCutter(obj, safe=True)
- if self.cutter is False or self.safeCutter is False:
- PathLog.error(translate('PathSurface', "Canceling 3D Surface operation. Error creating OCL cutter."))
- return
- toolDiam = self.cutter.getDiameter()
- self.cutOut = (toolDiam * (float(obj.StepOver) / 100.0))
- self.radius = toolDiam / 2.0
- self.gaps = [toolDiam, toolDiam, toolDiam]
-
- # Get height offset values for later use
- self.SafeHeightOffset = JOB.SetupSheet.SafeHeightOffset.Value
- self.ClearHeightOffset = JOB.SetupSheet.ClearanceHeightOffset.Value
-
- # Calculate default depthparams for operation
- self.depthParams = PathUtils.depth_params(obj.ClearanceHeight.Value, obj.SafeHeight.Value, obj.StartDepth.Value, obj.StepDown.Value, 0.0, obj.FinalDepth.Value)
- self.midDep = (obj.StartDepth.Value + obj.FinalDepth.Value) / 2.0
-
- # make circle for workplane
- self.wpc = Part.makeCircle(2.0)
-
- # Set deflection values for mesh generation
- try: # try/except is for Path Jobs created before GeometryTolerance
- self.deflection = JOB.GeometryTolerance.Value
- except AttributeError as ee:
- PathLog.warning('Error setting Mesh deflection: {}. Using PathPreferences.defaultGeometryTolerance().'.format(ee))
- import PathScripts.PathPreferences as PathPreferences
- self.deflection = PathPreferences.defaultGeometryTolerance()
-
- # Save model visibilities for restoration
- if FreeCAD.GuiUp:
- for m in range(0, len(JOB.Model.Group)):
- mNm = JOB.Model.Group[m].Name
- modelVisibility.append(FreeCADGui.ActiveDocument.getObject(mNm).Visibility)
-
- # Setup STL, model type, and bound box containers for each model in Job
- for m in range(0, len(JOB.Model.Group)):
- M = JOB.Model.Group[m]
- self.modelSTLs.append(False)
- self.safeSTLs.append(False)
- self.profileShapes.append(False)
- # Set bound box
- if obj.BoundBox == 'BaseBoundBox':
- if M.TypeId.startswith('Mesh'):
- self.modelTypes.append('M') # Mesh
- self.boundBoxes.append(M.Mesh.BoundBox)
- else:
- self.modelTypes.append('S') # Solid
- self.boundBoxes.append(M.Shape.BoundBox)
- elif obj.BoundBox == 'Stock':
- self.modelTypes.append('S') # Solid
- self.boundBoxes.append(JOB.Stock.Shape.BoundBox)
-
- # ###### MAIN COMMANDS FOR OPERATION ######
-
- # Begin processing obj.Base data and creating GCode
- # Process selected faces, if available
- pPM = self._preProcessModel(JOB, obj)
- if pPM is False:
- PathLog.error('Unable to pre-process obj.Base.')
- else:
- (FACES, VOIDS) = pPM
-
- # Create OCL.stl model objects
- self._prepareModelSTLs(JOB, obj)
-
- for m in range(0, len(JOB.Model.Group)):
- Mdl = JOB.Model.Group[m]
- if FACES[m] is False:
- PathLog.error('No data for model base: {}'.format(JOB.Model.Group[m].Label))
- else:
- if m > 0:
- # Raise to clearance between models
- CMDS.append(Path.Command('N (Transition to base: {}.)'.format(Mdl.Label)))
- CMDS.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid}))
- PathLog.info('Working on Model.Group[{}]: {}'.format(m, Mdl.Label))
- # make stock-model-voidShapes STL model for avoidance detection on transitions
- self._makeSafeSTL(JOB, obj, m, FACES[m], VOIDS[m])
- #time.sleep(0.2)
- # Process model/faces - OCL objects must be ready
- CMDS.extend(self._processCutAreas(JOB, obj, m, FACES[m], VOIDS[m]))
-
- # Save gcode produced
- self.commandlist.extend(CMDS)
-
- # ###### CLOSING COMMANDS FOR OPERATION ######
-
- # Delete temporary objects
- # Restore model visibilities for restoration
- if FreeCAD.GuiUp:
- FreeCADGui.ActiveDocument.getObject(tempGroupName).Visibility = False
- for m in range(0, len(JOB.Model.Group)):
- M = JOB.Model.Group[m]
- M.Visibility = modelVisibility[m]
-
- if deleteTempsFlag is True:
- for to in tempGroup.Group:
- if hasattr(to, 'Group'):
- for go in to.Group:
- FCAD.removeObject(go.Name)
- FCAD.removeObject(to.Name)
- FCAD.removeObject(tempGroupName)
- else:
- if len(tempGroup.Group) == 0:
- FCAD.removeObject(tempGroupName)
- else:
- tempGroup.purgeTouched()
-
- # Provide user feedback for gap sizes
- gaps = list()
- for g in self.gaps:
- if g != toolDiam:
- gaps.append(g)
- if len(gaps) > 0:
- obj.GapSizes = '{} mm'.format(gaps)
- else:
- if self.closedGap is True:
- obj.GapSizes = 'Closed gaps < Gap Threshold.'
- else:
- obj.GapSizes = 'No gaps identified.'
-
- # clean up class variables
- self.resetOpVariables()
- self.deleteOpVariables()
-
- self.modelSTLs = None
- self.safeSTLs = None
- self.modelTypes = None
- self.boundBoxes = None
- self.gaps = None
- self.closedGap = None
- self.SafeHeightOffset = None
- self.ClearHeightOffset = None
- self.depthParams = None
- self.midDep = None
- self.wpc = None
- self.deflection = None
- del self.modelSTLs
- del self.safeSTLs
- del self.modelTypes
- del self.boundBoxes
- del self.gaps
- del self.closedGap
- del self.SafeHeightOffset
- del self.ClearHeightOffset
- del self.depthParams
- del self.midDep
- del self.wpc
- del self.deflection
-
- execTime = time.time() - startTime
- PathLog.info('Operation time: {} sec.'.format(execTime))
-
- return True
-
- # Methods for constructing the cut area
- def _preProcessModel(self, JOB, obj):
- PathLog.debug('_preProcessModel()')
-
- FACES = list()
- VOIDS = list()
- fShapes = list()
- vShapes = list()
- preProcEr = translate('PathSurface', 'Error pre-processing Face')
- warnFinDep = translate('PathSurface', 'Final Depth might need to be lower. Internal features detected in Face')
- GRP = JOB.Model.Group
- lenGRP = len(GRP)
-
- # Crete place holders for each base model in Job
- for m in range(0, lenGRP):
- FACES.append(False)
- VOIDS.append(False)
- fShapes.append(False)
- vShapes.append(False)
-
- # The user has selected subobjects from the base. Pre-Process each.
- if obj.Base and len(obj.Base) > 0:
- PathLog.debug(' -obj.Base exists. Pre-processing for selected faces.')
-
- (FACES, VOIDS) = self._identifyFacesAndVoids(JOB, obj, FACES, VOIDS)
-
- # Cycle through each base model, processing faces for each
- for m in range(0, lenGRP):
- base = GRP[m]
- (mFS, mVS, mPS) = self._preProcessFacesAndVoids(obj, base, m, FACES, VOIDS)
- fShapes[m] = mFS
- vShapes[m] = mVS
- self.profileShapes[m] = mPS
- else:
- PathLog.debug(' -No obj.Base data.')
- for m in range(0, lenGRP):
- self.modelSTLs[m] = True
-
- # Process each model base, as a whole, as needed
- # PathLog.debug(' -Pre-processing all models in Job.')
- for m in range(0, lenGRP):
- if fShapes[m] is False:
- PathLog.debug(' -Pre-processing {} as a whole.'.format(GRP[m].Label))
- if obj.BoundBox == 'BaseBoundBox':
- base = GRP[m]
- elif obj.BoundBox == 'Stock':
- base = JOB.Stock
-
- pPEB = self._preProcessEntireBase(obj, base, m)
- if pPEB is False:
- PathLog.error(' -Failed to pre-process base as a whole.')
- else:
- (fcShp, prflShp) = pPEB
- if fcShp is not False:
- if fcShp is True:
- PathLog.debug(' -fcShp is True.')
- fShapes[m] = True
- else:
- fShapes[m] = [fcShp]
- if prflShp is not False:
- if fcShp is not False:
- PathLog.debug('vShapes[{}]: {}'.format(m, vShapes[m]))
- if vShapes[m] is not False:
- PathLog.debug(' -Cutting void from base profile shape.')
- adjPS = prflShp.cut(vShapes[m][0])
- self.profileShapes[m] = [adjPS]
- else:
- PathLog.debug(' -vShapes[m] is False.')
- self.profileShapes[m] = [prflShp]
- else:
- PathLog.debug(' -Saving base profile shape.')
- self.profileShapes[m] = [prflShp]
- PathLog.debug('self.profileShapes[{}]: {}'.format(m, self.profileShapes[m]))
- # Efor
-
- return (fShapes, vShapes)
-
- def _identifyFacesAndVoids(self, JOB, obj, F, V):
- TUPS = list()
- GRP = JOB.Model.Group
- lenGRP = len(GRP)
-
- # Separate selected faces into (base, face) tuples and flag model(s) for STL creation
- for (bs, SBS) in obj.Base:
- for sb in SBS:
- # Flag model for STL creation
- mdlIdx = None
- for m in range(0, lenGRP):
- if bs is GRP[m]:
- self.modelSTLs[m] = True
- mdlIdx = m
- break
- TUPS.append((mdlIdx, bs, sb)) # (model idx, base, sub)
-
- # Apply `AvoidXFaces` value
- faceCnt = len(TUPS)
- add = faceCnt - obj.AvoidLastX_Faces
- for bst in range(0, faceCnt):
- (m, base, sub) = TUPS[bst]
- shape = getattr(base.Shape, sub)
- if isinstance(shape, Part.Face):
- faceIdx = int(sub[4:]) - 1
- if bst < add:
- if F[m] is False:
- F[m] = list()
- F[m].append((shape, faceIdx))
- else:
- if V[m] is False:
- V[m] = list()
- V[m].append((shape, faceIdx))
- return (F, V)
-
- def _preProcessFacesAndVoids(self, obj, base, m, FACES, VOIDS):
- mFS = False
- mVS = False
- mPS = False
- mIFS = list()
- BB = base.Shape.BoundBox
-
- if FACES[m] is not False:
- isHole = False
- if obj.HandleMultipleFeatures == 'Collectively':
- cont = True
- fsL = list() # face shape list
- ifL = list() # avoid shape list
- outFCS = list()
-
- # Get collective envelope slice of selected faces
- for (fcshp, fcIdx) in FACES[m]:
- fNum = fcIdx + 1
- fsL.append(fcshp)
- gFW = self._getFaceWires(base, fcshp, fcIdx)
- if gFW is False:
- PathLog.debug('Failed to get wires from Face{}'.format(fNum))
- elif gFW[0] is False:
- PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum))
- else:
- ((otrFace, raised), intWires) = gFW
- outFCS.append(otrFace)
- if obj.InternalFeaturesCut is False:
- if intWires is not False:
- for (iFace, rsd) in intWires:
- ifL.append(iFace)
-
- PathLog.debug('Attempting to get cross-section of collective faces.')
- if len(outFCS) == 0:
- PathLog.error('Cannot process selected faces. Check horizontal surface exposure.'.format(fNum))
- cont = False
- else:
- cfsL = Part.makeCompound(outFCS)
-
- # Handle profile edges request
- if cont is True and obj.ProfileEdges != 'None':
- ofstVal = self._calculateOffsetValue(obj, isHole)
- psOfst = self._extractFaceOffset(cfsL, ofstVal)
- if psOfst is not False:
- mPS = [psOfst]
- if obj.ProfileEdges == 'Only':
- mFS = True
- cont = False
- else:
- PathLog.error(' -Failed to create profile geometry for selected faces.')
- cont = False
-
- if cont:
- if self.showDebugObjects is True:
- T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCollectiveShape')
- T.Shape = cfsL
- T.purgeTouched()
- self.tempGroup.addObject(T)
-
- ofstVal = self._calculateOffsetValue(obj, isHole)
- faceOfstShp = self._extractFaceOffset(cfsL, ofstVal)
- if faceOfstShp is False:
- PathLog.error(' -Failed to create offset face.')
- cont = False
-
- if cont:
- lenIfL = len(ifL)
- if obj.InternalFeaturesCut is False:
- if lenIfL == 0:
- PathLog.debug(' -No internal features saved.')
- else:
- if lenIfL == 1:
- casL = ifL[0]
- else:
- casL = Part.makeCompound(ifL)
- if self.showDebugObjects is True:
- C = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCompoundIntFeat')
- C.Shape = casL
- C.purgeTouched()
- self.tempGroup.addObject(C)
- ofstVal = self._calculateOffsetValue(obj, isHole=True)
- intOfstShp = self._extractFaceOffset(casL, ofstVal)
- mIFS.append(intOfstShp)
- # faceOfstShp = faceOfstShp.cut(intOfstShp)
-
- mFS = [faceOfstShp]
- # Eif
-
- elif obj.HandleMultipleFeatures == 'Individually':
- for (fcshp, fcIdx) in FACES[m]:
- cont = True
- fsL = list() # face shape list
- ifL = list() # avoid shape list
- fNum = fcIdx + 1
- outerFace = False
-
- gFW = self._getFaceWires(base, fcshp, fcIdx)
- if gFW is False:
- PathLog.debug('Failed to get wires from Face{}'.format(fNum))
- cont = False
- elif gFW[0] is False:
- PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum))
- cont = False
- outerFace = False
- else:
- ((otrFace, raised), intWires) = gFW
- outerFace = otrFace
- if obj.InternalFeaturesCut is False:
- if intWires is not False:
- for (iFace, rsd) in intWires:
- ifL.append(iFace)
-
- if outerFace is not False:
- PathLog.debug('Attempting to create offset face of Face{}'.format(fNum))
-
- if obj.ProfileEdges != 'None':
- ofstVal = self._calculateOffsetValue(obj, isHole)
- psOfst = self._extractFaceOffset(outerFace, ofstVal)
- if psOfst is not False:
- if mPS is False:
- mPS = list()
- mPS.append(psOfst)
- if obj.ProfileEdges == 'Only':
- if mFS is False:
- mFS = list()
- mFS.append(True)
- cont = False
- else:
- PathLog.error(' -Failed to create profile geometry for Face{}.'.format(fNum))
- cont = False
-
- if cont:
- ofstVal = self._calculateOffsetValue(obj, isHole)
- faceOfstShp = self._extractFaceOffset(outerFace, ofstVal)
-
- lenIfl = len(ifL)
- if obj.InternalFeaturesCut is False and lenIfl > 0:
- if lenIfl == 1:
- casL = ifL[0]
- else:
- casL = Part.makeCompound(ifL)
-
- ofstVal = self._calculateOffsetValue(obj, isHole=True)
- intOfstShp = self._extractFaceOffset(casL, ofstVal)
- mIFS.append(intOfstShp)
- # faceOfstShp = faceOfstShp.cut(intOfstShp)
-
- if mFS is False:
- mFS = list()
- mFS.append(faceOfstShp)
- # Eif
- # Efor
- # Eif
- # Eif
-
- if len(mIFS) > 0:
- if mVS is False:
- mVS = list()
- for ifs in mIFS:
- mVS.append(ifs)
-
- if VOIDS[m] is not False:
- PathLog.debug('Processing avoid faces.')
- cont = True
- isHole = False
- outFCS = list()
- intFEAT = list()
-
- for (fcshp, fcIdx) in VOIDS[m]:
- fNum = fcIdx + 1
- gFW = self._getFaceWires(base, fcshp, fcIdx)
- if gFW is False:
- PathLog.debug('Failed to get wires from avoid Face{}'.format(fNum))
- cont = False
- else:
- ((otrFace, raised), intWires) = gFW
- outFCS.append(otrFace)
- if obj.AvoidLastX_InternalFeatures is False:
- if intWires is not False:
- for (iFace, rsd) in intWires:
- intFEAT.append(iFace)
-
- lenOtFcs = len(outFCS)
- if lenOtFcs == 0:
- cont = False
- else:
- if lenOtFcs == 1:
- avoid = outFCS[0]
- else:
- avoid = Part.makeCompound(outFCS)
-
- if self.showDebugObjects is True:
- PathLog.debug('*** tmpAvoidArea')
- P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidEnvelope')
- P.Shape = avoid
- # P.recompute()
- P.purgeTouched()
- self.tempGroup.addObject(P)
-
- if cont:
- if self.showDebugObjects is True:
- PathLog.debug('*** tmpVoidCompound')
- P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidCompound')
- P.Shape = avoid
- # P.recompute()
- P.purgeTouched()
- self.tempGroup.addObject(P)
- ofstVal = self._calculateOffsetValue(obj, isHole, isVoid=True)
- avdOfstShp = self._extractFaceOffset(avoid, ofstVal)
- if avdOfstShp is False:
- PathLog.error('Failed to create collective offset avoid face.')
- cont = False
-
- if cont:
- avdShp = avdOfstShp
-
- if obj.AvoidLastX_InternalFeatures is False and len(intFEAT) > 0:
- if len(intFEAT) > 1:
- ifc = Part.makeCompound(intFEAT)
- else:
- ifc = intFEAT[0]
- ofstVal = self._calculateOffsetValue(obj, isHole=True)
- ifOfstShp = self._extractFaceOffset(ifc, ofstVal)
- if ifOfstShp is False:
- PathLog.error('Failed to create collective offset avoid internal features.')
- else:
- avdShp = avdOfstShp.cut(ifOfstShp)
-
- if mVS is False:
- mVS = list()
- mVS.append(avdShp)
-
-
- return (mFS, mVS, mPS)
-
- def _getFaceWires(self, base, fcshp, fcIdx):
- outFace = False
- INTFCS = list()
- fNum = fcIdx + 1
- # preProcEr = translate('PathSurface', 'Error pre-processing Face')
- warnFinDep = translate('PathSurface', 'Final Depth might need to be lower. Internal features detected in Face')
-
- PathLog.debug('_getFaceWires() from Face{}'.format(fNum))
- WIRES = self._extractWiresFromFace(base, fcshp)
- if WIRES is False:
- PathLog.error('Failed to extract wires from Face{}'.format(fNum))
- return False
-
- # Process remaining internal features, adding to FCS list
- lenW = len(WIRES)
- for w in range(0, lenW):
- (wire, rsd) = WIRES[w]
- PathLog.debug('Processing Wire{} in Face{}. isRaised: {}'.format(w + 1, fNum, rsd))
- if wire.isClosed() is False:
- PathLog.debug(' -wire is not closed.')
- else:
- slc = self._flattenWireToFace(wire)
- if slc is False:
- PathLog.error('FAILED to identify horizontal exposure on Face{}.'.format(fNum))
- else:
- if w == 0:
- outFace = (slc, rsd)
- else:
- # add to VOIDS so cutter avoids area.
- PathLog.warning(warnFinDep + str(fNum) + '.')
- INTFCS.append((slc, rsd))
- if len(INTFCS) == 0:
- return (outFace, False)
- else:
- return (outFace, INTFCS)
-
- def _preProcessEntireBase(self, obj, base, m):
- cont = True
- isHole = False
- prflShp = False
- # Create envelope, extract cross-section and make offset co-planar shape
- # baseEnv = PathUtils.getEnvelope(base.Shape, subshape=None, depthparams=self.depthParams)
-
- try:
- baseEnv = PathUtils.getEnvelope(partshape=base.Shape, subshape=None, depthparams=self.depthParams) # Produces .Shape
- except Exception as ee:
- PathLog.error(str(ee))
- shell = base.Shape.Shells[0]
- solid = Part.makeSolid(shell)
- try:
- baseEnv = PathUtils.getEnvelope(partshape=solid, subshape=None, depthparams=self.depthParams) # Produces .Shape
- except Exception as eee:
- PathLog.error(str(eee))
- cont = False
- #time.sleep(0.2)
-
- if cont:
- csFaceShape = self._getShapeSlice(baseEnv)
- if csFaceShape is False:
- PathLog.debug('_getShapeSlice(baseEnv) failed')
- csFaceShape = self._getCrossSection(baseEnv)
- if csFaceShape is False:
- PathLog.debug('_getCrossSection(baseEnv) failed')
- csFaceShape = self._getSliceFromEnvelope(baseEnv)
- if csFaceShape is False:
- PathLog.error('Failed to slice baseEnv shape.')
- cont = False
-
- if cont is True and obj.ProfileEdges != 'None':
- PathLog.debug(' -Attempting profile geometry for model base.')
- ofstVal = self._calculateOffsetValue(obj, isHole)
- psOfst = self._extractFaceOffset(csFaceShape, ofstVal)
- if psOfst is not False:
- if obj.ProfileEdges == 'Only':
- return (True, psOfst)
- prflShp = psOfst
- else:
- PathLog.error(' -Failed to create profile geometry.')
- cont = False
-
- if cont:
- ofstVal = self._calculateOffsetValue(obj, isHole)
- faceOffsetShape = self._extractFaceOffset(csFaceShape, ofstVal)
- if faceOffsetShape is False:
- PathLog.error('_extractFaceOffset() failed.')
- else:
- faceOffsetShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - faceOffsetShape.BoundBox.ZMin))
- return (faceOffsetShape, prflShp)
- return False
-
- def _extractWiresFromFace(self, base, fc):
- '''_extractWiresFromFace(base, fc) ...
- Attempts to return all closed wires within a parent face, including the outer most wire of the parent.
- The wires are ordered by area. Each wire is also categorized as a pocket(False) or raised protrusion(True).
- '''
- PathLog.debug('_extractWiresFromFace()')
-
- WIRES = list()
- lenWrs = len(fc.Wires)
- PathLog.debug(' -Wire count: {}'.format(lenWrs))
-
- def index0(tup):
- return tup[0]
-
- # Cycle through wires in face
- for w in range(0, lenWrs):
- PathLog.debug(' -Analyzing wire_{}'.format(w + 1))
- wire = fc.Wires[w]
- checkEdges = False
- cont = True
-
- # Check for closed edges (circles, ellipses, etc...)
- for E in wire.Edges:
- if E.isClosed() is True:
- checkEdges = True
- break
-
- if checkEdges is True:
- PathLog.debug(' -checkEdges is True')
- for e in range(0, len(wire.Edges)):
- edge = wire.Edges[e]
- if edge.isClosed() is True and edge.Mass > 0.01:
- PathLog.debug(' -Found closed edge')
- raised = False
- ip = self._isPocket(base, fc, edge)
- if ip is False:
- raised = True
- ebb = edge.BoundBox
- eArea = ebb.XLength * ebb.YLength
- F = Part.Face(Part.Wire([edge]))
- WIRES.append((eArea, F.Wires[0], raised))
- cont = False
-
- if cont:
- PathLog.debug(' -cont is True')
- # If only one wire and not checkEdges, return first wire
- if lenWrs == 1:
- return [(wire, False)]
-
- raised = False
- wbb = wire.BoundBox
- wArea = wbb.XLength * wbb.YLength
- if w > 0:
- ip = self._isPocket(base, fc, wire)
- if ip is False:
- raised = True
- WIRES.append((wArea, Part.Wire(wire.Edges), raised))
-
- nf = len(WIRES)
- if nf > 0:
- PathLog.debug(' -number of wires found is {}'.format(nf))
- if nf == 1:
- (area, W, raised) = WIRES[0]
- return [(W, raised)]
- else:
- sortedWIRES = sorted(WIRES, key=index0, reverse=True)
- return [(W, raised) for (area, W, raised) in sortedWIRES] # outer, then inner by area size
-
- return False
-
- def _calculateOffsetValue(self, obj, isHole, isVoid=False):
- '''_calculateOffsetValue(obj, isHole, isVoid) ... internal function.
- Calculate the offset for the Path.Area() function.'''
- JOB = PathUtils.findParentJob(obj)
- tolrnc = JOB.GeometryTolerance.Value
-
- if isVoid is False:
- if isHole is True:
- offset = -1 * obj.InternalFeaturesAdjustment.Value
- offset += self.radius + (tolrnc / 10.0)
- else:
- offset = -1 * obj.BoundaryAdjustment.Value
- if obj.BoundaryEnforcement is True:
- offset += self.radius + (tolrnc / 10.0)
- else:
- offset -= self.radius + (tolrnc / 10.0)
- offset = 0.0 - offset
- else:
- offset = -1 * obj.BoundaryAdjustment.Value
- offset += self.radius + (tolrnc / 10.0)
-
- return offset
-
- def _extractFaceOffset(self, fcShape, offset):
- '''_extractFaceOffset(fcShape, offset) ... internal function.
- Original _buildPathArea() version copied from PathAreaOp.py module. This version is modified.
- Adjustments made based on notes by @sliptonic at this webpage: https://github.com/sliptonic/FreeCAD/wiki/PathArea-notes.'''
- PathLog.debug('_extractFaceOffset()')
-
- if fcShape.BoundBox.ZMin != 0.0:
- fcShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - fcShape.BoundBox.ZMin))
-
- areaParams = {}
- areaParams['Offset'] = offset
- areaParams['Fill'] = 1
- areaParams['Coplanar'] = 0
- areaParams['SectionCount'] = 1 # -1 = full(all per depthparams??) sections
- areaParams['Reorient'] = True
- areaParams['OpenMode'] = 0
- areaParams['MaxArcPoints'] = 400 # 400
- areaParams['Project'] = True
-
- area = Path.Area() # Create instance of Area() class object
- # area.setPlane(PathUtils.makeWorkplane(fcShape)) # Set working plane
- area.setPlane(PathUtils.makeWorkplane(self.wpc)) # Set working plane to normal at Z=1
- area.add(fcShape)
- area.setParams(**areaParams) # set parameters
-
- offsetShape = area.getShape()
- wCnt = len(offsetShape.Wires)
- if wCnt == 0:
- return False
- elif wCnt == 1:
- ofstFace = Part.Face(offsetShape.Wires[0])
- else:
- W = list()
- for wr in offsetShape.Wires:
- W.append(Part.Face(wr))
- ofstFace = Part.makeCompound(W)
-
- return ofstFace # offsetShape
-
- def _isPocket(self, b, f, w):
- '''_isPocket(b, f, w)...
- Attempts to determine if the wire(w) in face(f) of base(b) is a pocket or raised protrusion.
- Returns True if pocket, False if raised protrusion.'''
- e = w.Edges[0]
- for fi in range(0, len(b.Shape.Faces)):
- face = b.Shape.Faces[fi]
- for ei in range(0, len(face.Edges)):
- edge = face.Edges[ei]
- if e.isSame(edge) is True:
- if f is face:
- # Alternative: run loop to see if all edges are same
- pass # same source face, look for another
- else:
- if face.CenterOfMass.z < f.CenterOfMass.z:
- return True
- return False
-
- def _flattenWireToFace(self, wire):
- PathLog.debug('_flattenWireToFace()')
- if wire.isClosed() is False:
- PathLog.debug(' -wire.isClosed() is False')
- return False
-
- # If wire is planar horizontal, convert to a face and return
- if wire.BoundBox.ZLength == 0.0:
- slc = Part.Face(wire)
- return slc
-
- # Attempt to create a new wire for manipulation, if not, use original
- newWire = Part.Wire(wire.Edges)
- if newWire.isClosed() is True:
- nWire = newWire
- else:
- PathLog.debug(' -newWire.isClosed() is False')
- nWire = wire
-
- # Attempt extrusion, and then try a manual slice and then cross-section
- ext = self._getExtrudedShape(nWire)
- if ext is False:
- PathLog.debug('_getExtrudedShape() failed')
- else:
- slc = self._getShapeSlice(ext)
- if slc is not False:
- return slc
- cs = self._getCrossSection(ext, True)
- if cs is not False:
- return cs
-
- # Attempt creating an envelope, and then try a manual slice and then cross-section
- env = self._getShapeEnvelope(nWire)
- if env is False:
- PathLog.debug('_getShapeEnvelope() failed')
- else:
- slc = self._getShapeSlice(env)
- if slc is not False:
- return slc
- cs = self._getCrossSection(env, True)
- if cs is not False:
- return cs
-
- # Attempt creating a projection
- slc = self._getProjectedFace(nWire)
- if slc is False:
- PathLog.debug('_getProjectedFace() failed')
- else:
- return slc
-
- return False
-
- def _getExtrudedShape(self, wire):
- PathLog.debug('_getExtrudedShape()')
- wBB = wire.BoundBox
- extFwd = math.floor(2.0 * wBB.ZLength) + 10.0
-
- try:
- # slower, but renders collective faces correctly. Method 5 in TESTING
- shell = wire.extrude(FreeCAD.Vector(0.0, 0.0, extFwd))
- except Exception as ee:
- PathLog.error(' -extrude wire failed: \n{}'.format(ee))
- return False
-
- SHP = Part.makeSolid(shell)
- return SHP
-
- def _getShapeSlice(self, shape):
- PathLog.debug('_getShapeSlice()')
-
- bb = shape.BoundBox
- mid = (bb.ZMin + bb.ZMax) / 2.0
- xmin = bb.XMin - 1.0
- xmax = bb.XMax + 1.0
- ymin = bb.YMin - 1.0
- ymax = bb.YMax + 1.0
- p1 = FreeCAD.Vector(xmin, ymin, mid)
- p2 = FreeCAD.Vector(xmax, ymin, mid)
- p3 = FreeCAD.Vector(xmax, ymax, mid)
- p4 = FreeCAD.Vector(xmin, ymax, mid)
-
- e1 = Part.makeLine(p1, p2)
- e2 = Part.makeLine(p2, p3)
- e3 = Part.makeLine(p3, p4)
- e4 = Part.makeLine(p4, p1)
- face = Part.Face(Part.Wire([e1, e2, e3, e4]))
- fArea = face.BoundBox.XLength * face.BoundBox.YLength # face.Wires[0].Area
- sArea = shape.BoundBox.XLength * shape.BoundBox.YLength
- midArea = (fArea + sArea) / 2.0
-
- slcShp = shape.common(face)
- slcArea = slcShp.BoundBox.XLength * slcShp.BoundBox.YLength
-
- if slcArea < midArea:
- for W in slcShp.Wires:
- if W.isClosed() is False:
- PathLog.debug(' -wire.isClosed() is False')
- return False
- if len(slcShp.Wires) == 1:
- wire = slcShp.Wires[0]
- slc = Part.Face(wire)
- slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin))
- return slc
- else:
- fL = list()
- for W in slcShp.Wires:
- slc = Part.Face(W)
- slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin))
- fL.append(slc)
- comp = Part.makeCompound(fL)
- if self.showDebugObjects is True:
- PathLog.debug('*** tmpSliceCompound')
- P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpSliceCompound')
- P.Shape = comp
- # P.recompute()
- P.purgeTouched()
- self.tempGroup.addObject(P)
- return comp
-
- PathLog.debug(' -slcArea !< midArea')
- PathLog.debug(' -slcShp.Edges count: {}. Might be a vertically oriented face.'.format(len(slcShp.Edges)))
- return False
-
- def _getProjectedFace(self, wire):
- PathLog.debug('_getProjectedFace()')
- F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpProjectionWire')
- F.Shape = wire
- F.purgeTouched()
- self.tempGroup.addObject(F)
- try:
- prj = Draft.makeShape2DView(F, FreeCAD.Vector(0, 0, 1))
- prj.recompute()
- prj.purgeTouched()
- self.tempGroup.addObject(prj)
- except Exception as ee:
- PathLog.error(str(ee))
- return False
- else:
- pWire = Part.Wire(prj.Shape.Edges)
- if pWire.isClosed() is False:
- # PathLog.debug(' -pWire.isClosed() is False')
- return False
- slc = Part.Face(pWire)
- slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin))
- return slc
- return False
-
- def _getCrossSection(self, shape, withExtrude=False):
- PathLog.debug('_getCrossSection()')
- wires = list()
- bb = shape.BoundBox
- mid = (bb.ZMin + bb.ZMax) / 2.0
-
- for i in shape.slice(FreeCAD.Vector(0, 0, 1), mid):
- wires.append(i)
-
- if len(wires) > 0:
- comp = Part.Compound(wires) # produces correct cross-section wire !
- comp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - comp.BoundBox.ZMin))
- csWire = comp.Wires[0]
- if csWire.isClosed() is False:
- PathLog.debug(' -comp.Wires[0] is not closed')
- return False
- if withExtrude is True:
- ext = self._getExtrudedShape(csWire)
- CS = self._getShapeSlice(ext)
- if CS is False:
- return False
- else:
- CS = Part.Face(csWire)
- CS.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - CS.BoundBox.ZMin))
- return CS
- else:
- PathLog.debug(' -No wires from .slice() method')
-
- return False
-
- def _getShapeEnvelope(self, shape):
- PathLog.debug('_getShapeEnvelope()')
-
- wBB = shape.BoundBox
- extFwd = wBB.ZLength + 10.0
- minz = wBB.ZMin
- maxz = wBB.ZMin + extFwd
- stpDwn = (maxz - minz) / 4.0
- dep_par = PathUtils.depth_params(maxz + 5.0, maxz + 3.0, maxz, stpDwn, 0.0, minz)
-
- try:
- env = PathUtils.getEnvelope(partshape=shape, depthparams=dep_par) # Produces .Shape
- except Exception as ee:
- PathLog.error('try: PathUtils.getEnvelope() failed.\n' + str(ee))
- return False
- else:
- return env
-
- return False
-
- def _getSliceFromEnvelope(self, env):
- PathLog.debug('_getSliceFromEnvelope()')
- eBB = env.BoundBox
- extFwd = eBB.ZLength + 10.0
- maxz = eBB.ZMin + extFwd
-
- maxMax = env.Edges[0].BoundBox.ZMin
- emax = math.floor(maxz - 1.0)
- E = list()
- for e in range(0, len(env.Edges)):
- emin = env.Edges[e].BoundBox.ZMin
- if emin > emax:
- E.append(env.Edges[e])
- tf = Part.Face(Part.Wire(Part.__sortEdges__(E)))
- tf.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - tf.BoundBox.ZMin))
-
- return tf
-
- def _prepareModelSTLs(self, JOB, obj):
- PathLog.debug('_prepareModelSTLs()')
- for m in range(0, len(JOB.Model.Group)):
- M = JOB.Model.Group[m]
-
- # PathLog.debug(f" -self.modelTypes[{m}] == 'M'")
- if self.modelTypes[m] == 'M':
- #TODO: test if this works
- facets = M.Mesh.Facets.Points
- else:
- facets = Part.getFacets(M.Shape)
-
- if self.modelSTLs[m] is True:
- stl = ocl.STLSurf()
-
- for tri in facets:
- t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2]),
- ocl.Point(tri[1][0], tri[1][1], tri[1][2]),
- ocl.Point(tri[2][0], tri[2][1], tri[2][2]))
- stl.addTriangle(t)
- self.modelSTLs[m] = stl
- return
-
- def _makeSafeSTL(self, JOB, obj, mdlIdx, faceShapes, voidShapes):
- '''_makeSafeSTL(JOB, obj, mdlIdx, faceShapes, voidShapes)...
- Creates and OCL.stl object with combined data with waste stock,
- model, and avoided faces. Travel lines can be checked against this
- STL object to determine minimum travel height to clear stock and model.'''
- PathLog.debug('_makeSafeSTL()')
-
- fuseShapes = list()
- Mdl = JOB.Model.Group[mdlIdx]
- FCAD = FreeCAD.ActiveDocument
- mBB = Mdl.Shape.BoundBox
- sBB = JOB.Stock.Shape.BoundBox
-
- # add Model shape to safeSTL shape
- fuseShapes.append(Mdl.Shape)
-
- if obj.BoundBox == 'BaseBoundBox':
- cont = False
- extFwd = (sBB.ZLength)
- zmin = mBB.ZMin
- zmax = mBB.ZMin + extFwd
- stpDwn = (zmax - zmin) / 4.0
- dep_par = PathUtils.depth_params(zmax + 5.0, zmax + 3.0, zmax, stpDwn, 0.0, zmin)
-
- try:
- envBB = PathUtils.getEnvelope(partshape=Mdl.Shape, depthparams=dep_par) # Produces .Shape
- cont = True
- except Exception as ee:
- PathLog.error(str(ee))
- shell = Mdl.Shape.Shells[0]
- solid = Part.makeSolid(shell)
- try:
- envBB = PathUtils.getEnvelope(partshape=solid, depthparams=dep_par) # Produces .Shape
- cont = True
- except Exception as eee:
- PathLog.error(str(eee))
-
- if cont:
- stckWst = JOB.Stock.Shape.cut(envBB)
- if obj.BoundaryAdjustment > 0.0:
- cmpndFS = Part.makeCompound(faceShapes)
- baBB = PathUtils.getEnvelope(partshape=cmpndFS, depthparams=self.depthParams) # Produces .Shape
- adjStckWst = stckWst.cut(baBB)
- else:
- adjStckWst = stckWst
- fuseShapes.append(adjStckWst)
- else:
- PathLog.warning('Path transitions might not avoid the model. Verify paths.')
- #time.sleep(0.3)
- else:
- # If boundbox is Job.Stock, add hidden pad under stock as base plate
- toolDiam = self.cutter.getDiameter()
- zMin = JOB.Stock.Shape.BoundBox.ZMin
- xMin = JOB.Stock.Shape.BoundBox.XMin - toolDiam
- yMin = JOB.Stock.Shape.BoundBox.YMin - toolDiam
- bL = JOB.Stock.Shape.BoundBox.XLength + (2 * toolDiam)
- bW = JOB.Stock.Shape.BoundBox.YLength + (2 * toolDiam)
- bH = 1.0
- crnr = FreeCAD.Vector(xMin, yMin, zMin - 1.0)
- B = Part.makeBox(bL, bW, bH, crnr, FreeCAD.Vector(0, 0, 1))
- fuseShapes.append(B)
-
- if voidShapes is not False:
- voidComp = Part.makeCompound(voidShapes)
- voidEnv = PathUtils.getEnvelope(partshape=voidComp, depthparams=self.depthParams) # Produces .Shape
- fuseShapes.append(voidEnv)
-
- fused = Part.makeCompound(fuseShapes)
-
- if self.showDebugObjects is True:
- T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'safeSTLShape')
- T.Shape = fused
- T.purgeTouched()
- self.tempGroup.addObject(T)
-
- facets = Part.getFacets(fused)
-
- stl = ocl.STLSurf()
- for tri in facets:
- t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2]),
- ocl.Point(tri[1][0], tri[1][1], tri[1][2]),
- ocl.Point(tri[2][0], tri[2][1], tri[2][2]))
- stl.addTriangle(t)
-
- self.safeSTLs[mdlIdx] = stl
-
- def _processCutAreas(self, JOB, obj, mdlIdx, FCS, VDS):
- '''_processCutAreas(JOB, obj, mdlIdx, FCS, VDS)...
- This method applies any avoided faces or regions to the selected faces.
- It then calls the correct scan method depending on the ScanType property.'''
- PathLog.debug('_processCutAreas()')
-
- final = list()
- base = JOB.Model.Group[mdlIdx]
-
- # Process faces Collectively or Individually
- if obj.HandleMultipleFeatures == 'Collectively':
- if FCS is True:
- COMP = False
- else:
- ADD = Part.makeCompound(FCS)
- if VDS is not False:
- DEL = Part.makeCompound(VDS)
- COMP = ADD.cut(DEL)
- else:
- COMP = ADD
-
- if obj.ScanType == 'Planar':
- final.extend(self._processPlanarOp(JOB, obj, mdlIdx, COMP, 0))
- elif obj.ScanType == 'Rotational':
- final.extend(self._processRotationalOp(obj, base, COMP))
-
- elif obj.HandleMultipleFeatures == 'Individually':
- for fsi in range(0, len(FCS)):
- fShp = FCS[fsi]
- # self.deleteOpVariables(all=False)
- self.resetOpVariables(all=False)
-
- if fShp is True:
- COMP = False
- else:
- ADD = Part.makeCompound([fShp])
- if VDS is not False:
- DEL = Part.makeCompound(VDS)
- COMP = ADD.cut(DEL)
- else:
- COMP = ADD
-
- if obj.ScanType == 'Planar':
- final.extend(self._processPlanarOp(JOB, obj, mdlIdx, COMP, fsi))
- elif obj.ScanType == 'Rotational':
- final.extend(self._processRotationalOp(JOB, obj, mdlIdx, COMP))
- COMP = None
- # Eif
-
- return final
-
- # Methods for creating path geometry
- def _processPlanarOp(self, JOB, obj, mdlIdx, cmpdShp, fsi):
- '''_processPlanarOp(JOB, obj, mdlIdx, cmpdShp)...
- This method compiles the main components for the procedural portion of a planar operation (non-rotational).
- It creates the OCL PathDropCutter objects: model and safeTravel.
- It makes the necessary facial geometries for the actual cut area.
- It calls the correct Single or Multi-pass method as needed.
- It returns the gcode for the operation. '''
- PathLog.debug('_processPlanarOp()')
- final = list()
- SCANDATA = list()
- # base = JOB.Model.Group[mdlIdx]
-
- # Compute number and size of stepdowns, and final depth
- if obj.LayerMode == 'Single-pass':
- depthparams = [obj.FinalDepth.Value]
- elif obj.LayerMode == 'Multi-pass':
- depthparams = [i for i in self.depthParams]
- lenDP = len(depthparams)
-
- # Prepare PathDropCutter objects with STL data
- pdc = self._planarGetPDC(self.modelSTLs[mdlIdx], depthparams[lenDP - 1], obj.SampleInterval.Value)
- safePDC = self._planarGetPDC(self.safeSTLs[mdlIdx],
- depthparams[lenDP - 1], obj.SampleInterval.Value, useSafeCutter=False)
-
- profScan = list()
- if obj.ProfileEdges != 'None':
- prflShp = self.profileShapes[mdlIdx][fsi]
- if prflShp is False:
- PathLog.error('No profile shape is False.')
- return list()
- if self.showDebugObjects is True:
- P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpNewProfileShape')
- P.Shape = prflShp
- # P.recompute()
- P.purgeTouched()
- self.tempGroup.addObject(P)
- # get offset path geometry and perform OCL scan with that geometry
- pathOffsetGeom = self._planarMakeProfileGeom(obj, prflShp)
- if pathOffsetGeom is False:
- PathLog.error('No profile geometry returned.')
- return list()
- profScan = [self._planarPerformOclScan(obj, pdc, pathOffsetGeom, offsetPoints=True)]
-
- geoScan = list()
- if obj.ProfileEdges != 'Only':
- if self.showDebugObjects is True:
- F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCutArea')
- F.Shape = cmpdShp
- # F.recompute()
- F.purgeTouched()
- self.tempGroup.addObject(F)
- # get internal path geometry and perform OCL scan with that geometry
- pathGeom = self._planarMakePathGeom(obj, cmpdShp)
- if pathGeom is False:
- PathLog.error('No path geometry returned.')
- return list()
- geoScan = self._planarPerformOclScan(obj, pdc, pathGeom, offsetPoints=False)
-
- if obj.ProfileEdges == 'Only': # ['None', 'Only', 'First', 'Last']
- SCANDATA.extend(profScan)
- if obj.ProfileEdges == 'None':
- SCANDATA.extend(geoScan)
- if obj.ProfileEdges == 'First':
- SCANDATA.extend(profScan)
- SCANDATA.extend(geoScan)
- if obj.ProfileEdges == 'Last':
- SCANDATA.extend(geoScan)
- SCANDATA.extend(profScan)
-
- # Apply depth offset
- if obj.DepthOffset.Value != 0.0:
- self._planarApplyDepthOffset(SCANDATA, obj.DepthOffset.Value)
-
- if len(SCANDATA) == 0:
- PathLog.error('No scan data to convert to Gcode.')
- return list()
-
- # If cut pattern is `Circular`, there are zero(almost zero) straight lines to optimize
- # Store initial `OptimizeLinearPaths` value for later restoration
- self.preOLP = obj.OptimizeLinearPaths
- if obj.CutPattern in ['Circular', 'CircularZigZag']:
- obj.OptimizeLinearPaths = False
-
- # Process OCL scan data
- if obj.LayerMode == 'Single-pass':
- final.extend(self._planarDropCutSingle(JOB, obj, pdc, safePDC, depthparams, SCANDATA))
- elif obj.LayerMode == 'Multi-pass':
- final.extend(self._planarDropCutMulti(JOB, obj, pdc, safePDC, depthparams, SCANDATA))
-
- # If cut pattern is `Circular`, restore initial OLP value
- if obj.CutPattern in ['Circular', 'CircularZigZag']:
- obj.OptimizeLinearPaths = self.preOLP
-
- # Raise to safe height between individual faces.
- if obj.HandleMultipleFeatures == 'Individually':
- final.insert(0, Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
-
- return final
-
- def _planarMakePathGeom(self, obj, faceShp):
- '''_planarMakePathGeom(obj, faceShp)...
- Creates the line/arc cut pattern geometry and returns the intersection with the received faceShp.
- The resulting intersecting line/arc geometries are then converted to lines or arcs for OCL.'''
- PathLog.debug('_planarMakePathGeom()')
- GeoSet = list()
-
- # Apply drop cutter extra offset and set the max and min XY area of the operation
- xmin = faceShp.BoundBox.XMin
- xmax = faceShp.BoundBox.XMax
- ymin = faceShp.BoundBox.YMin
- ymax = faceShp.BoundBox.YMax
- zmin = faceShp.BoundBox.ZMin
- zmax = faceShp.BoundBox.ZMax
-
- # Compute weighted center of mass of all faces combined
- fCnt = 0
- totArea = 0.0
- zeroCOM = FreeCAD.Vector(0.0, 0.0, 0.0)
- for F in faceShp.Faces:
- comF = F.CenterOfMass
- areaF = F.Area
- totArea += areaF
- fCnt += 1
- zeroCOM = zeroCOM.add(FreeCAD.Vector(comF.x, comF.y, 0.0).multiply(areaF))
- if fCnt == 0:
- PathLog.error(translate('PathSurface', 'Cannot calculate the Center Of Mass. Using Center of Boundbox.'))
- zeroCOM = FreeCAD.Vector((xmin + xmax) / 2.0, (ymin + ymax) / 2.0, 0.0)
- else:
- avgArea = totArea / fCnt
- zeroCOM.multiply(1 / fCnt)
- zeroCOM.multiply(1 / avgArea)
- COM = FreeCAD.Vector(zeroCOM.x, zeroCOM.y, 0.0)
-
- # get X, Y, Z spans; Compute center of rotation
- deltaX = abs(xmax-xmin)
- deltaY = abs(ymax-ymin)
- deltaZ = abs(zmax-zmin)
- deltaC = math.sqrt(deltaX**2 + deltaY**2)
- lineLen = deltaC + (2.0 * self.cutter.getDiameter()) # Line length to span boundbox diag with 2x cutter diameter extra on each end
- halfLL = math.ceil(lineLen / 2.0)
- cutPasses = math.ceil(lineLen / self.cutOut) + 1 # Number of lines(passes) required to cover lineLen
- halfPasses = math.ceil(cutPasses / 2.0)
- bbC = faceShp.BoundBox.Center
-
- # Generate the Draft line/circle sets to be intersected with the cut-face-area
- if obj.CutPattern in ['ZigZag', 'Line']:
- MaxLC = -1
- centRot = FreeCAD.Vector(0.0, 0.0, 0.0) # Bottom left corner of face/selection/model
- cAng = math.atan(deltaX / deltaY) # BoundaryBox angle
-
- # Determine end points and create top lines
- x1 = centRot.x - halfLL
- x2 = centRot.x + halfLL
- diag = None
- if obj.CutPatternAngle == 0 or obj.CutPatternAngle == 180:
- MaxLC = math.floor(deltaY / self.cutOut)
- diag = deltaY
- elif obj.CutPatternAngle == 90 or obj.CutPatternAngle == 270:
- MaxLC = math.floor(deltaX / self.cutOut)
- diag = deltaX
- else:
- perpDist = math.cos(cAng - math.radians(obj.CutPatternAngle)) * deltaC
- MaxLC = math.floor(perpDist / self.cutOut)
- diag = perpDist
- y1 = centRot.y + diag
- # y2 = y1
-
- p1 = FreeCAD.Vector(x1, y1, 0.0)
- p2 = FreeCAD.Vector(x2, y1, 0.0)
- topLineTuple = (p1, p2)
- ny1 = centRot.y - diag
- n1 = FreeCAD.Vector(x1, ny1, 0.0)
- n2 = FreeCAD.Vector(x2, ny1, 0.0)
- negTopLineTuple = (n1, n2)
-
- # Create end points for set of lines to intersect with cross-section face
- pntTuples = list()
- for lc in range((-1 * (halfPasses - 1)), halfPasses + 1):
- # if lc == (cutPasses - MaxLC - 1):
- # pntTuples.append(negTopLineTuple)
- # if lc == (MaxLC + 1):
- # pntTuples.append(topLineTuple)
- x1 = centRot.x - halfLL
- x2 = centRot.x + halfLL
- y1 = centRot.y + (lc * self.cutOut)
- # y2 = y1
- p1 = FreeCAD.Vector(x1, y1, 0.0)
- p2 = FreeCAD.Vector(x2, y1, 0.0)
- pntTuples.append( (p1, p2) )
-
- # Convert end points to lines
- for (p1, p2) in pntTuples:
- line = Part.makeLine(p1, p2)
- GeoSet.append(line)
- elif obj.CutPattern in ['Circular', 'CircularZigZag']:
- zTgt = faceShp.BoundBox.ZMin
- axisRot = FreeCAD.Vector(0.0, 0.0, 1.0)
- cntr = FreeCAD.Placement()
- cntr.Rotation = FreeCAD.Rotation(axisRot, 0.0)
-
- if obj.CircularCenterAt == 'CenterOfMass':
- cntr.Base = FreeCAD.Vector(COM.x, COM.y, zTgt) # COM # Use center of Mass
- elif obj.CircularCenterAt == 'CenterOfBoundBox':
- cent = faceShp.BoundBox.Center
- cntr.Base = FreeCAD.Vector(cent.x, cent.y, zTgt)
- elif obj.CircularCenterAt == 'XminYmin':
- cntr.Base = FreeCAD.Vector(faceShp.BoundBox.XMin, faceShp.BoundBox.YMin, zTgt)
- elif obj.CircularCenterAt == 'Custom':
- newCent = FreeCAD.Vector(obj.CircularCenterCustom.x, obj.CircularCenterCustom.y, zTgt)
- cntr.Base = newCent
-
- # recalculate cutPasses value, if need be
- radialPasses = halfPasses
- if obj.CircularCenterAt != 'CenterOfBoundBox':
- # make 4 corners of boundbox in XY plane, find which is greatest distance to new circular center
- EBB = faceShp.BoundBox
- CORNERS = [
- FreeCAD.Vector(EBB.XMin, EBB.YMin, 0.0),
- FreeCAD.Vector(EBB.XMin, EBB.YMax, 0.0),
- FreeCAD.Vector(EBB.XMax, EBB.YMax, 0.0),
- FreeCAD.Vector(EBB.XMax, EBB.YMin, 0.0),
- ]
- dMax = 0.0
- for c in range(0, 4):
- dist = CORNERS[c].sub(cntr.Base).Length
- if dist > dMax:
- dMax = dist
- lineLen = dMax + (2.0 * self.cutter.getDiameter()) # Line length to span boundbox diag with 2x cutter diameter extra on each end
- radialPasses = math.ceil(lineLen / self.cutOut) + 1 # Number of lines(passes) required to cover lineLen
-
- # Update COM point and current CircularCenter
- if obj.CircularCenterAt != 'Custom':
- obj.CircularCenterCustom = cntr.Base
-
- minRad = self.cutter.getDiameter() * 0.45
- siX3 = 3 * obj.SampleInterval.Value
- minRadSI = (siX3 / 2.0) / math.pi
- if minRad < minRadSI:
- minRad = minRadSI
-
- # Make small center circle to start pattern
- if obj.StepOver > 50:
- circle = Part.makeCircle(minRad, cntr.Base)
- GeoSet.append(circle)
-
- for lc in range(1, radialPasses + 1):
- rad = (lc * self.cutOut)
- if rad >= minRad:
- circle = Part.makeCircle(rad, cntr.Base)
- GeoSet.append(circle)
- # Efor
- COM = cntr.Base
- # Eif
-
- if obj.CutPatternReversed is True:
- GeoSet.reverse()
-
- if faceShp.BoundBox.ZMin != 0.0:
- faceShp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - faceShp.BoundBox.ZMin))
-
- # Create compound object to bind all lines in Lineset
- geomShape = Part.makeCompound(GeoSet)
-
- # Position and rotate the Line and ZigZag geometry
- if obj.CutPattern in ['Line', 'ZigZag']:
- if obj.CutPatternAngle != 0.0:
- geomShape.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), obj.CutPatternAngle)
- geomShape.Placement.Base = FreeCAD.Vector(bbC.x, bbC.y, 0.0 - geomShape.BoundBox.ZMin)
-
- if self.showDebugObjects is True:
- F = FreeCAD.ActiveDocument.addObject('Part::Feature','tmpGeometrySet')
- F.Shape = geomShape
- F.purgeTouched()
- self.tempGroup.addObject(F)
-
- # Identify intersection of cross-section face and lineset
- cmnShape = faceShp.common(geomShape)
-
- if self.showDebugObjects is True:
- F = FreeCAD.ActiveDocument.addObject('Part::Feature','tmpPathGeometry')
- F.Shape = cmnShape
- F.purgeTouched()
- self.tempGroup.addObject(F)
-
- self.tmpCOM = FreeCAD.Vector(COM.x, COM.y, faceShp.BoundBox.ZMin)
- return cmnShape
-
- def _planarMakeProfileGeom(self, obj, subShp):
- PathLog.debug('_planarMakeProfileGeom()')
-
- offsetLists = list()
- dist = obj.SampleInterval.Value / 5.0
- defl = obj.SampleInterval.Value / 5.0
-
- # Reference https://forum.freecadweb.org/viewtopic.php?t=28861#p234939
- for fc in subShp.Faces:
- # Reverse order of wires in each face - inside to outside
- for w in range(len(fc.Wires) - 1, -1, -1):
- W = fc.Wires[w]
- PNTS = W.discretize(Distance=dist)
- # PNTS = W.discretize(Deflection=defl)
- if self.CutClimb is True:
- PNTS.reverse()
- offsetLists.append(PNTS)
-
- return offsetLists
-
- def _planarPerformOclScan(self, obj, pdc, pathGeom, offsetPoints=False):
- '''_planarPerformOclScan(obj, pdc, pathGeom, offsetPoints=False)...
- Switching function for calling the appropriate path-geometry to OCL points conversion function
- for the various cut patterns.'''
- PathLog.debug('_planarPerformOclScan()')
- SCANS = list()
-
- if offsetPoints is True:
- PNTSET = self._pathGeomToOffsetPointSet(obj, pathGeom)
- for D in PNTSET:
- stpOvr = list()
- ofst = list()
- for I in D:
- if I == 'BRK':
- stpOvr.append(ofst)
- stpOvr.append(I)
- ofst = list()
- else:
- # D format is ((p1, p2), (p3, p4))
- (A, B) = I
- ofst.extend(self._planarDropCutScan(pdc, A, B))
- if len(ofst) > 0:
- stpOvr.append(ofst)
- SCANS.extend(stpOvr)
- elif obj.CutPattern == 'Line':
- stpOvr = list()
- PNTSET = self._pathGeomToLinesPointSet(obj, pathGeom)
- for D in PNTSET:
- for I in D:
- if I == 'BRK':
- stpOvr.append(I)
- else:
- # D format is ((p1, p2), (p3, p4))
- (A, B) = I
- stpOvr.append(self._planarDropCutScan(pdc, A, B))
- SCANS.append(stpOvr)
- stpOvr = list()
- elif obj.CutPattern == 'ZigZag':
- stpOvr = list()
- PNTSET = self._pathGeomToZigzagPointSet(obj, pathGeom)
- for (dirFlg, LNS) in PNTSET:
- for SEG in LNS:
- if SEG == 'BRK':
- stpOvr.append(SEG)
- else:
- # D format is ((p1, p2), (p3, p4))
- (A, B) = SEG
- stpOvr.append(self._planarDropCutScan(pdc, A, B))
- SCANS.append(stpOvr)
- stpOvr = list()
- elif obj.CutPattern in ['Circular', 'CircularZigZag']:
- # PNTSET is list, by stepover.
- # Each stepover is a list containing arc/loop descriptions, (sp, ep, cp)
- PNTSET = self._pathGeomToArcPointSet(obj, pathGeom)
-
- for so in range(0, len(PNTSET)):
- stpOvr = list()
- erFlg = False
- (aTyp, dirFlg, ARCS) = PNTSET[so]
-
- if dirFlg == 1: # 1
- cMode = True
- else:
- cMode = False
-
- for a in range(0, len(ARCS)):
- Arc = ARCS[a]
- if Arc == 'BRK':
- stpOvr.append('BRK')
- else:
- scan = self._planarCircularDropCutScan(pdc, Arc, cMode)
- if scan is False:
- erFlg = True
- else:
- if aTyp == 'L':
- scan.append(FreeCAD.Vector(scan[0].x, scan[0].y, scan[0].z))
- stpOvr.append(scan)
- if erFlg is False:
- SCANS.append(stpOvr)
-
- return SCANS
-
- def _pathGeomToOffsetPointSet(self, obj, compGeoShp):
- '''_pathGeomToOffsetPointSet(obj, compGeoShp)...
- Convert a compound set of 3D profile segmented wires to 2D segments, applying linear optimization.'''
- PathLog.debug('_pathGeomToOffsetPointSet()')
-
- LINES = list()
- optimize = obj.OptimizeLinearPaths
- ofstCnt = len(compGeoShp)
-
- # Cycle through offeset loops
- for ei in range(0, ofstCnt):
- OS = compGeoShp[ei]
- lenOS = len(OS)
-
- if ei > 0:
- LINES.append('BRK')
-
- fp = FreeCAD.Vector(OS[0].x, OS[0].y, OS[0].z)
- OS.append(fp)
-
- # Cycle through points in each loop
- prev = OS[0]
- pnt = OS[1]
- for v in range(1, lenOS):
- nxt = OS[v + 1]
- if optimize is True:
- iPOL = prev.isOnLineSegment(nxt, pnt)
- if iPOL is True:
- pnt = nxt
- else:
- tup = ((prev.x, prev.y), (pnt.x, pnt.y))
- LINES.append(tup)
- prev = pnt
- pnt = nxt
- else:
- tup = ((prev.x, prev.y), (pnt.x, pnt.y))
- LINES.append(tup)
- prev = pnt
- pnt = nxt
- if iPOL is True:
- tup = ((prev.x, prev.y), (pnt.x, pnt.y))
- LINES.append(tup)
- # Efor
-
- return [LINES]
-
- def _pathGeomToLinesPointSet(self, obj, compGeoShp):
- '''_pathGeomToLinesPointSet(obj, compGeoShp)...
- Convert a compound set of sequential line segments to directionally-oriented collinear groupings.'''
- PathLog.debug('_pathGeomToLinesPointSet()')
- # Extract intersection line segments for return value as list()
- LINES = list()
- inLine = list()
- chkGap = False
- lnCnt = 0
- ec = len(compGeoShp.Edges)
- cutClimb = self.CutClimb
- toolDiam = 2.0 * self.radius
- cpa = obj.CutPatternAngle
-
- edg0 = compGeoShp.Edges[0]
- p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y)
- p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y)
- if cutClimb is True:
- tup = (p2, p1)
- lst = FreeCAD.Vector(p1[0], p1[1], 0.0)
- else:
- tup = (p1, p2)
- lst = FreeCAD.Vector(p2[0], p2[1], 0.0)
- inLine.append(tup)
- sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point
-
- for ei in range(1, ec):
- chkGap = False
- edg = compGeoShp.Edges[ei] # Get edge for vertexes
- v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y) # vertex 0
- v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y) # vertex 1
-
- ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point
- cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (first / middle point)
- iC = sp.isOnLineSegment(ep, cp)
- if iC is True:
- inLine.append('BRK')
- chkGap = True
- else:
- if cutClimb is True:
- inLine.reverse()
- LINES.append(inLine) # Save inLine segments
- lnCnt += 1
- inLine = list() # reset collinear container
- if cutClimb is True:
- sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0)
- else:
- sp = ep
-
- if cutClimb is True:
- tup = (v2, v1)
- if chkGap is True:
- gap = abs(toolDiam - lst.sub(ep).Length)
- lst = cp
- else:
- tup = (v1, v2)
- if chkGap is True:
- gap = abs(toolDiam - lst.sub(cp).Length)
- lst = ep
-
- if chkGap is True:
- if gap < obj.GapThreshold.Value:
- b = inLine.pop() # pop off 'BRK' marker
- (vA, vB) = inLine.pop() # pop off previous line segment for combining with current
- tup = (vA, tup[1])
- self.closedGap = True
- else:
- # PathLog.debug('---- Gap: {} mm'.format(gap))
- gap = round(gap, 6)
- if gap < self.gaps[0]:
- self.gaps.insert(0, gap)
- self.gaps.pop()
- inLine.append(tup)
- # Efor
- lnCnt += 1
- if cutClimb is True:
- inLine.reverse()
- LINES.append(inLine) # Save inLine segments
-
- # Handle last inLine set, reversing it.
- if obj.CutPatternReversed is True:
- if cpa != 0.0 and cpa % 90.0 == 0.0:
- F = LINES.pop(0)
- rev = list()
- for iL in F:
- if iL == 'BRK':
- rev.append(iL)
- else:
- (p1, p2) = iL
- rev.append((p2, p1))
- rev.reverse()
- LINES.insert(0, rev)
-
- isEven = lnCnt % 2
- if isEven == 0:
- PathLog.debug('Line count is ODD.')
- else:
- PathLog.debug('Line count is even.')
-
- return LINES
-
- def _pathGeomToZigzagPointSet(self, obj, compGeoShp):
- '''_pathGeomToZigzagPointSet(obj, compGeoShp)...
- Convert a compound set of sequential line segments to directionally-oriented collinear groupings
- with a ZigZag directional indicator included for each collinear group.'''
- PathLog.debug('_pathGeomToZigzagPointSet()')
- # Extract intersection line segments for return value as list()
- LINES = list()
- inLine = list()
- lnCnt = 0
- chkGap = False
- ec = len(compGeoShp.Edges)
- toolDiam = 2.0 * self.radius
-
- if self.CutClimb is True:
- dirFlg = -1
- else:
- dirFlg = 1
-
- edg0 = compGeoShp.Edges[0]
- p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y)
- p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y)
- if dirFlg == 1:
- tup = (p1, p2)
- lst = FreeCAD.Vector(p2[0], p2[1], 0.0)
- sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point
- else:
- tup = (p2, p1)
- lst = FreeCAD.Vector(p1[0], p1[1], 0.0)
- sp = FreeCAD.Vector(p2[0], p2[1], 0.0) # start point
- inLine.append(tup)
- otr = lst
-
- for ei in range(1, ec):
- edg = compGeoShp.Edges[ei]
- v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y)
- v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y)
-
- cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (start point of segment)
- ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point
- iC = sp.isOnLineSegment(ep, cp)
- if iC is True:
- inLine.append('BRK')
- chkGap = True
- gap = abs(toolDiam - lst.sub(cp).Length)
- else:
- chkGap = False
- if dirFlg == -1:
- inLine.reverse()
- LINES.append((dirFlg, inLine))
- lnCnt += 1
- dirFlg = -1 * dirFlg # Change zig to zag
- inLine = list() # reset collinear container
- sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0)
- otr = ep
-
- lst = ep
- if dirFlg == 1:
- tup = (v1, v2)
- else:
- tup = (v2, v1)
-
- if chkGap is True:
- if gap < obj.GapThreshold.Value:
- b = inLine.pop() # pop off 'BRK' marker
- (vA, vB) = inLine.pop() # pop off previous line segment for combining with current
- if dirFlg == 1:
- tup = (vA, tup[1])
- else:
- #tup = (vA, tup[1])
- #tup = (tup[1], vA)
- tup = (tup[0], vB)
- self.closedGap = True
- else:
- gap = round(gap, 6)
- if gap < self.gaps[0]:
- self.gaps.insert(0, gap)
- self.gaps.pop()
- inLine.append(tup)
- # Efor
- lnCnt += 1
-
- # Fix directional issue with LAST line when line count is even
- isEven = lnCnt % 2
- if isEven == 0: # Changed to != with 90 degree CutPatternAngle
- PathLog.debug('Line count is even.')
- else:
- PathLog.debug('Line count is ODD.')
- dirFlg = -1 * dirFlg
- if obj.CutPatternReversed is False:
- if self.CutClimb is True:
- dirFlg = -1 * dirFlg
-
- if obj.CutPatternReversed is True:
- dirFlg = -1 * dirFlg
-
- # Handle last inLine list
- if dirFlg == 1:
- rev = list()
- for iL in inLine:
- if iL == 'BRK':
- rev.append(iL)
- else:
- (p1, p2) = iL
- rev.append((p2, p1))
-
- if obj.CutPatternReversed is False:
- rev.reverse()
- else:
- rev2 = list()
- for iL in rev:
- if iL == 'BRK':
- rev2.append(iL)
- else:
- (p1, p2) = iL
- rev2.append((p2, p1))
- rev2.reverse()
- rev = rev2
-
- LINES.append((dirFlg, rev))
- else:
- LINES.append((dirFlg, inLine))
-
- return LINES
-
- def _pathGeomToArcPointSet(self, obj, compGeoShp):
- '''_pathGeomToArcPointSet(obj, compGeoShp)...
- Convert a compound set of arcs/circles to a set of directionally-oriented arc end points
- and the corresponding center point.'''
- # Extract intersection line segments for return value as list()
- PathLog.debug('_pathGeomToArcPointSet()')
- ARCS = list()
- stpOvrEI = list()
- segEI = list()
- isSame = False
- sameRad = None
- COM = self.tmpCOM
- toolDiam = 2.0 * self.radius
- ec = len(compGeoShp.Edges)
-
- def gapDist(sp, ep):
- X = (ep[0] - sp[0])**2
- Y = (ep[1] - sp[1])**2
- Z = (ep[2] - sp[2])**2
- # return math.sqrt(X + Y + Z)
- return math.sqrt(X + Y) # the 'z' value is zero in both points
-
- # Separate arc data into Loops and Arcs
- for ei in range(0, ec):
- edg = compGeoShp.Edges[ei]
- if edg.Closed is True:
- stpOvrEI.append(('L', ei, False))
- else:
- if isSame is False:
- segEI.append(ei)
- isSame = True
- pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0)
- sameRad = pnt.sub(COM).Length
- else:
- # Check if arc is co-radial to current SEGS
- pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0)
- if abs(sameRad - pnt.sub(COM).Length) > 0.00001:
- isSame = False
-
- if isSame is True:
- segEI.append(ei)
- else:
- # Move co-radial arc segments
- stpOvrEI.append(['A', segEI, False])
- # Start new list of arc segments
- segEI = [ei]
- isSame = True
- pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0)
- sameRad = pnt.sub(COM).Length
- # Process trailing `segEI` data, if available
- if isSame is True:
- stpOvrEI.append(['A', segEI, False])
-
- # Identify adjacent arcs with y=0 start/end points that connect
- for so in range(0, len(stpOvrEI)):
- SO = stpOvrEI[so]
- if SO[0] == 'A':
- startOnAxis = list()
- endOnAxis = list()
- EI = SO[1] # list of corresponding compGeoShp.Edges indexes
-
- # Identify startOnAxis and endOnAxis arcs
- for i in range(0, len(EI)):
- ei = EI[i] # edge index
- E = compGeoShp.Edges[ei] # edge object
- if abs(COM.y - E.Vertexes[0].Y) < 0.00001:
- startOnAxis.append((i, ei, E.Vertexes[0]))
- elif abs(COM.y - E.Vertexes[1].Y) < 0.00001:
- endOnAxis.append((i, ei, E.Vertexes[1]))
-
- # Look for connections between startOnAxis and endOnAxis arcs. Consolidate data when connected
- lenSOA = len(startOnAxis)
- lenEOA = len(endOnAxis)
- if lenSOA > 0 and lenEOA > 0:
- delIdxs = list()
- lstFindIdx = 0
- for soa in range(0, lenSOA):
- (iS, eiS, vS) = startOnAxis[soa]
- for eoa in range(0, len(endOnAxis)):
- (iE, eiE, vE) = endOnAxis[eoa]
- dist = vE.X - vS.X
- if abs(dist) < 0.00001: # They connect on axis at same radius
- SO[2] = (eiE, eiS)
- break
- elif dist > 0:
- break # stop searching
- # Eif
- # Eif
- # Efor
-
- # Construct arc data tuples for OCL
- dirFlg = 1
- # cutPat = obj.CutPattern
- if self.CutClimb is False: # True yields Climb when set to Conventional
- dirFlg = -1
-
- # Cycle through stepOver data
- for so in range(0, len(stpOvrEI)):
- SO = stpOvrEI[so]
- if SO[0] == 'L': # L = Loop/Ring/Circle
- lei = SO[1] # loop Edges index
- v1 = compGeoShp.Edges[lei].Vertexes[0]
-
- space = obj.SampleInterval.Value / 2.0
-
- p1 = FreeCAD.Vector(v1.X, v1.Y, v1.Z)
- sp = (v1.X, v1.Y, 0.0)
- rad = p1.sub(COM).Length
- spcRadRatio = space/rad
- if spcRadRatio < 1.0:
- tolrncAng = math.asin(spcRadRatio)
- else:
- tolrncAng = 0.999998 * math.pi
- X = COM.x + (rad * math.cos(tolrncAng))
- Y = v1.Y - space # rad * math.sin(tolrncAng)
-
- sp = (v1.X, v1.Y, 0.0)
- ep = (X, Y, 0.0)
- cp = (COM.x, COM.y, 0.0)
- if dirFlg == 1:
- arc = (sp, ep, cp)
- else:
- arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction))
- ARCS.append(('L', dirFlg, [arc]))
- else: # SO[0] == 'A' A = Arc
- PRTS = list()
- EI = SO[1] # list of corresponding Edges indexes
- CONN = SO[2] # list of corresponding connected edges tuples (iE, iS)
- chkGap = False
- lst = None
-
- if CONN is not False:
- (iE, iS) = CONN
- v1 = compGeoShp.Edges[iE].Vertexes[0]
- v2 = compGeoShp.Edges[iS].Vertexes[1]
- sp = (v1.X, v1.Y, 0.0)
- ep = (v2.X, v2.Y, 0.0)
- cp = (COM.x, COM.y, 0.0)
- if dirFlg == 1:
- arc = (sp, ep, cp)
- lst = ep
- else:
- arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction))
- lst = sp
- PRTS.append(arc)
- # Pop connected edge index values from arc segments index list
- iEi = EI.index(iE)
- iSi = EI.index(iS)
- if iEi > iSi:
- EI.pop(iEi)
- EI.pop(iSi)
- else:
- EI.pop(iSi)
- EI.pop(iEi)
- if len(EI) > 0:
- PRTS.append('BRK')
- chkGap = True
- cnt = 0
- for ei in EI:
- if cnt > 0:
- PRTS.append('BRK')
- chkGap = True
- v1 = compGeoShp.Edges[ei].Vertexes[0]
- v2 = compGeoShp.Edges[ei].Vertexes[1]
- sp = (v1.X, v1.Y, 0.0)
- ep = (v2.X, v2.Y, 0.0)
- cp = (COM.x, COM.y, 0.0)
- if dirFlg == 1:
- arc = (sp, ep, cp)
- if chkGap is True:
- gap = abs(toolDiam - gapDist(lst, sp)) # abs(toolDiam - lst.sub(sp).Length)
- lst = ep
- else:
- arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction))
- if chkGap is True:
- gap = abs(toolDiam - gapDist(lst, ep)) # abs(toolDiam - lst.sub(ep).Length)
- lst = sp
- if chkGap is True:
- if gap < obj.GapThreshold.Value:
- b = PRTS.pop() # pop off 'BRK' marker
- (vA, vB, vC) = PRTS.pop() # pop off previous arc segment for combining with current
- arc = (vA, arc[1], vC)
- self.closedGap = True
- else:
- # PathLog.debug('---- Gap: {} mm'.format(gap))
- gap = round(gap, 6)
- if gap < self.gaps[0]:
- self.gaps.insert(0, gap)
- self.gaps.pop()
- PRTS.append(arc)
- cnt += 1
-
- if dirFlg == -1:
- PRTS.reverse()
-
- ARCS.append(('A', dirFlg, PRTS))
- # Eif
- if obj.CutPattern == 'CircularZigZag':
- dirFlg = -1 * dirFlg
- # Efor
-
- return ARCS
-
- def _planarDropCutScan(self, pdc, A, B):
- #PNTS = list()
- (x1, y1) = A
- (x2, y2) = B
- path = ocl.Path() # create an empty path object
- p1 = ocl.Point(x1, y1, 0) # start-point of line
- p2 = ocl.Point(x2, y2, 0) # end-point of line
- lo = ocl.Line(p1, p2) # line-object
- path.append(lo) # add the line to the path
- pdc.setPath(path)
- pdc.run() # run dropcutter algorithm on path
- CLP = pdc.getCLPoints()
- PNTS = [FreeCAD.Vector(p.x, p.y, p.z) for p in CLP]
- return PNTS # pdc.getCLPoints()
-
- def _planarCircularDropCutScan(self, pdc, Arc, cMode):
- PNTS = list()
- path = ocl.Path() # create an empty path object
- (sp, ep, cp) = Arc
-
- # process list of segment tuples (vect, vect)
- path = ocl.Path() # create an empty path object
- p1 = ocl.Point(sp[0], sp[1], 0) # start point of arc
- p2 = ocl.Point(ep[0], ep[1], 0) # end point of arc
- C = ocl.Point(cp[0], cp[1], 0) # center point of arc
- ao = ocl.Arc(p1, p2, C, cMode) # arc object
- path.append(ao) # add the arc to the path
- pdc.setPath(path)
- pdc.run() # run dropcutter algorithm on path
- CLP = pdc.getCLPoints()
-
- # Convert OCL object data to FreeCAD vectors
- for p in CLP:
- PNTS.append(FreeCAD.Vector(p.x, p.y, p.z))
-
- return PNTS
-
- # Main planar scan functions
- def _planarDropCutSingle(self, JOB, obj, pdc, safePDC, depthparams, SCANDATA):
- PathLog.debug('_planarDropCutSingle()')
-
- GCODE = [Path.Command('N (Beginning of Single-pass layer.)', {})]
- tolrnc = JOB.GeometryTolerance.Value
- prevDepth = obj.SafeHeight.Value
- lenDP = len(depthparams)
- lenSCANDATA = len(SCANDATA)
- gDIR = ['G3', 'G2']
-
- if self.CutClimb is True:
- gDIR = ['G2', 'G3']
-
- # Set `ProfileEdges` specific trigger indexes
- peIdx = lenSCANDATA # off by default
- if obj.ProfileEdges == 'Only':
- peIdx = -1
- elif obj.ProfileEdges == 'First':
- peIdx = 0
- elif obj.ProfileEdges == 'Last':
- peIdx = lenSCANDATA - 1
-
- # Send cutter to x,y position of first point on first line
- first = SCANDATA[0][0][0] # [step][item][point]
- GCODE.append(Path.Command('G0', {'X': first.x, 'Y': first.y, 'F': self.horizRapid}))
-
- # Cycle through step-over sections (line segments or arcs)
- odd = True
- lstStpEnd = None
- prevDepth = obj.SafeHeight.Value # Not used for Single-pass
- for so in range(0, lenSCANDATA):
- cmds = list()
- PRTS = SCANDATA[so]
- lenPRTS = len(PRTS)
- first = PRTS[0][0] # first point of arc/line stepover group
- start = PRTS[0][0] # will change with each line/arc segment
- last = None
- cmds.append(Path.Command('N (Begin step {}.)'.format(so), {}))
-
- if so > 0:
- if obj.CutPattern == 'CircularZigZag':
- if odd is True:
- odd = False
- else:
- odd = True
- minTrnsHght = self._getMinSafeTravelHeight(safePDC, lstStpEnd, first) # Check safe travel height against fullSTL
- # cmds.append(Path.Command('N (Transition: last, first: {}, {}: minSTH: {})'.format(lstStpEnd, first, minTrnsHght), {}))
- cmds.extend(self._stepTransitionCmds(obj, lstStpEnd, first, minTrnsHght, tolrnc))
-
- # Override default `OptimizeLinearPaths` behavior to allow `ProfileEdges` optimization
- if so == peIdx or peIdx == -1:
- obj.OptimizeLinearPaths = self.preOLP
-
- # Cycle through current step-over parts
- for i in range(0, lenPRTS):
- prt = PRTS[i]
- lenPrt = len(prt)
- if prt == 'BRK':
- nxtStart = PRTS[i + 1][0]
- minSTH = self._getMinSafeTravelHeight(safePDC, last, nxtStart) # Check safe travel height against fullSTL
- cmds.append(Path.Command('N (Break)', {}))
- cmds.extend(self._breakCmds(obj, last, nxtStart, minSTH, tolrnc))
- else:
- cmds.append(Path.Command('N (part {}.)'.format(i + 1), {}))
- start = prt[0]
- last = prt[lenPrt - 1]
- if so == peIdx or peIdx == -1:
- cmds.extend(self._planarSinglepassProcess(obj, prt))
- elif obj.CutPattern in ['Circular', 'CircularZigZag'] and obj.CircularUseG2G3 is True and lenPrt > 2:
- (rtnVal, gcode) = self._arcsToG2G3(prt, lenPrt, odd, gDIR, tolrnc)
- if rtnVal is True:
- cmds.extend(gcode)
- else:
- cmds.extend(self._planarSinglepassProcess(obj, prt))
- else:
- cmds.extend(self._planarSinglepassProcess(obj, prt))
- cmds.append(Path.Command('N (End of step {}.)'.format(so), {}))
- GCODE.extend(cmds) # save line commands
- lstStpEnd = last
-
- # Return `OptimizeLinearPaths` to disabled
- if so == peIdx or peIdx == -1:
- if obj.CutPattern in ['Circular', 'CircularZigZag']:
- obj.OptimizeLinearPaths = False
- # Efor
-
- return GCODE
-
- def _planarSinglepassProcess(self, obj, PNTS):
- if obj.OptimizeLinearPaths:
- # first item will be compared to the last point, but I think that should work
- output = [Path.Command('G1', {'X': PNTS[i].x, 'Y': PNTS[i].y, 'Z': PNTS[i].z, 'F': self.horizFeed})
- for i in range(0, len(PNTS) - 1)
- if not PNTS[i].isOnLineSegment(PNTS[i -1],PNTS[i + 1])]
- output.append(Path.Command('G1', {'X': PNTS[-1].x, 'Y': PNTS[-1].y, 'Z': PNTS[-1].z, 'F': self.horizFeed}))
- else:
- output = [Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed}) for pnt in PNTS]
-
- return output
-
- def _planarDropCutMulti(self, JOB, obj, pdc, safePDC, depthparams, SCANDATA):
- GCODE = [Path.Command('N (Beginning of Multi-pass layers.)', {})]
- tolrnc = JOB.GeometryTolerance.Value
- lenDP = len(depthparams)
- prevDepth = depthparams[0]
- lenSCANDATA = len(SCANDATA)
- gDIR = ['G3', 'G2']
-
- if self.CutClimb is True:
- gDIR = ['G2', 'G3']
-
- # Set `ProfileEdges` specific trigger indexes
- peIdx = lenSCANDATA # off by default
- if obj.ProfileEdges == 'Only':
- peIdx = -1
- elif obj.ProfileEdges == 'First':
- peIdx = 0
- elif obj.ProfileEdges == 'Last':
- peIdx = lenSCANDATA - 1
-
- # Process each layer in depthparams
- prvLyrFirst = None
- prvLyrLast = None
- lastPrvStpLast = None
- actvLyrs = 0
- for lyr in range(0, lenDP):
- odd = True # ZigZag directional switch
- lyrHasCmds = False
- lstStpEnd = None
- actvSteps = 0
- LYR = list()
- prvStpFirst = None
- if lyr > 0:
- if prvStpLast is not None:
- lastPrvStpLast = prvStpLast
- prvStpLast = None
- lyrDep = depthparams[lyr]
- PathLog.debug('Multi-pass lyrDep: {}'.format(round(lyrDep, 4)))
-
- # Cycle through step-over sections (line segments or arcs)
- for so in range(0, len(SCANDATA)):
- SO = SCANDATA[so]
- lenSO = len(SO)
-
- # Pre-process step-over parts for layer depth and holds
- ADJPRTS = list()
- LMAX = list()
- soHasPnts = False
- brkFlg = False
- for i in range(0, lenSO):
- prt = SO[i]
- lenPrt = len(prt)
- if prt == 'BRK':
- if brkFlg is True:
- ADJPRTS.append(prt)
- LMAX.append(prt)
- brkFlg = False
- else:
- (PTS, lMax) = self._planarMultipassPreProcess(obj, prt, prevDepth, lyrDep)
- if len(PTS) > 0:
- ADJPRTS.append(PTS)
- soHasPnts = True
- brkFlg = True
- LMAX.append(lMax)
- # Efor
- lenAdjPrts = len(ADJPRTS)
-
- # Process existing parts within current step over
- prtsHasCmds = False
- stepHasCmds = False
- prtsCmds = list()
- stpOvrCmds = list()
- transCmds = list()
- if soHasPnts is True:
- first = ADJPRTS[0][0] # first point of arc/line stepover group
-
- # Manage step over transition and CircularZigZag direction
- if so > 0:
- # PathLog.debug(' stepover index: {}'.format(so))
- # Control ZigZag direction
- if obj.CutPattern == 'CircularZigZag':
- if odd is True:
- odd = False
- else:
- odd = True
- # Control step over transition
- if prvStpLast is None:
- prvStpLast = lastPrvStpLast
- minTrnsHght = self._getMinSafeTravelHeight(safePDC, prvStpLast, first, minDep=None) # Check safe travel height against fullSTL
- transCmds.append(Path.Command('N (--Step {} transition)'.format(so), {}))
- transCmds.extend(self._stepTransitionCmds(obj, prvStpLast, first, minTrnsHght, tolrnc))
-
- # Override default `OptimizeLinearPaths` behavior to allow `ProfileEdges` optimization
- if so == peIdx or peIdx == -1:
- obj.OptimizeLinearPaths = self.preOLP
-
- # Cycle through current step-over parts
- for i in range(0, lenAdjPrts):
- prt = ADJPRTS[i]
- lenPrt = len(prt)
- # PathLog.debug(' adj parts index - lenPrt: {} - {}'.format(i, lenPrt))
- if prt == 'BRK' and prtsHasCmds is True:
- nxtStart = ADJPRTS[i + 1][0]
- minSTH = self._getMinSafeTravelHeight(safePDC, last, nxtStart, minDep=None) # Check safe travel height against fullSTL
- prtsCmds.append(Path.Command('N (--Break)', {}))
- prtsCmds.extend(self._breakCmds(obj, last, nxtStart, minSTH, tolrnc))
- else:
- segCmds = False
- prtsCmds.append(Path.Command('N (part {})'.format(i + 1), {}))
- last = prt[lenPrt - 1]
- if so == peIdx or peIdx == -1:
- segCmds = self._planarSinglepassProcess(obj, prt)
- elif obj.CutPattern in ['Circular', 'CircularZigZag'] and obj.CircularUseG2G3 is True and lenPrt > 2:
- (rtnVal, gcode) = self._arcsToG2G3(prt, lenPrt, odd, gDIR, tolrnc)
- if rtnVal is True:
- segCmds = gcode
- else:
- segCmds = self._planarSinglepassProcess(obj, prt)
- else:
- segCmds = self._planarSinglepassProcess(obj, prt)
-
- if segCmds is not False:
- prtsCmds.extend(segCmds)
- prtsHasCmds = True
- prvStpLast = last
- # Eif
- # Efor
- # Eif
-
- # Return `OptimizeLinearPaths` to disabled
- if so == peIdx or peIdx == -1:
- if obj.CutPattern in ['Circular', 'CircularZigZag']:
- obj.OptimizeLinearPaths = False
-
- # Compile step over(prts) commands
- if prtsHasCmds is True:
- stepHasCmds = True
- actvSteps += 1
- prvStpFirst = first
- stpOvrCmds.extend(transCmds)
- stpOvrCmds.append(Path.Command('N (Begin step {}.)'.format(so), {}))
- stpOvrCmds.append(Path.Command('G0', {'X': first.x, 'Y': first.y, 'F': self.horizRapid}))
- stpOvrCmds.extend(prtsCmds)
- stpOvrCmds.append(Path.Command('N (End of step {}.)'.format(so), {}))
-
- # Layer transition at first active step over in current layer
- if actvSteps == 1:
- prvLyrFirst = first
- LYR.append(Path.Command('N (Layer {} begins)'.format(lyr), {}))
- if lyr > 0:
- LYR.append(Path.Command('N (Layer transition)', {}))
- LYR.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
- LYR.append(Path.Command('G0', {'X': first.x, 'Y': first.y, 'F': self.horizRapid}))
-
- if stepHasCmds is True:
- lyrHasCmds = True
- LYR.extend(stpOvrCmds)
- # Eif
-
- # Close layer, saving commands, if any
- if lyrHasCmds is True:
- prvLyrLast = last
- GCODE.extend(LYR) # save line commands
- GCODE.append(Path.Command('N (End of layer {})'.format(lyr), {}))
-
- # Set previous depth
- prevDepth = lyrDep
- # Efor
-
- PathLog.debug('Multi-pass op has {} layers (step downs).'.format(lyr + 1))
-
- return GCODE
-
- def _planarMultipassPreProcess(self, obj, LN, prvDep, layDep):
- ALL = list()
- PTS = list()
- brkFlg = False
- optLinTrans = obj.OptimizeStepOverTransitions
- safe = math.ceil(obj.SafeHeight.Value)
-
- if optLinTrans is True:
- for P in LN:
- ALL.append(P)
- # Handle layer depth AND hold points
- if P.z <= layDep:
- PTS.append(FreeCAD.Vector(P.x, P.y, layDep))
- elif P.z > prvDep:
- PTS.append(FreeCAD.Vector(P.x, P.y, safe))
- else:
- PTS.append(FreeCAD.Vector(P.x, P.y, P.z))
- # Efor
- else:
- for P in LN:
- ALL.append(P)
- # Handle layer depth only
- if P.z <= layDep:
- PTS.append(FreeCAD.Vector(P.x, P.y, layDep))
- else:
- PTS.append(FreeCAD.Vector(P.x, P.y, P.z))
- # Efor
-
- if optLinTrans is True:
- # Remove leading and trailing Hold Points
- popList = list()
- for i in range(0, len(PTS)): # identify leading string
- if PTS[i].z == safe:
- popList.append(i)
- else:
- break
- popList.sort(reverse=True)
- for p in popList: # Remove hold points
- PTS.pop(p)
- ALL.pop(p)
- popList = list()
- for i in range(len(PTS) - 1, -1, -1): # identify trailing string
- if PTS[i].z == safe:
- popList.append(i)
- else:
- break
- popList.sort(reverse=True)
- for p in popList: # Remove hold points
- PTS.pop(p)
- ALL.pop(p)
-
- # Determine max Z height for remaining points on line
- lMax = obj.FinalDepth.Value
- if len(ALL) > 0:
- lMax = ALL[0].z
- for P in ALL:
- if P.z > lMax:
- lMax = P.z
-
- return (PTS, lMax)
-
- def _planarMultipassProcess(self, obj, PNTS, lMax):
- output = list()
- optimize = obj.OptimizeLinearPaths
- safe = math.ceil(obj.SafeHeight.Value)
- lenPNTS = len(PNTS)
- lastPNTS = lenPNTS - 1
- prcs = True
- onHold = False
- onLine = False
- clrScnLn = lMax + 2.0
-
- # Initialize first three points
- nxt = None
- pnt = PNTS[0]
- prev = FreeCAD.Vector(-442064564.6, 258539656553.27, 3538553425.847)
-
- # Add temp end point
- PNTS.append(FreeCAD.Vector(-4895747464.6, -25855763553.2, 35865763425))
-
- # Begin processing ocl points list into gcode
- for i in range(0, lenPNTS):
- prcs = True
- nxt = PNTS[i + 1]
-
- if pnt.z == safe:
- prcs = False
- if onHold is False:
- onHold = True
- output.append( Path.Command('N (Start hold)', {}) )
- output.append( Path.Command('G0', {'Z': clrScnLn, 'F': self.vertRapid}) )
- else:
- if onHold is True:
- onHold = False
- output.append( Path.Command('N (End hold)', {}) )
- output.append( Path.Command('G0', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizRapid}) )
-
- # Process point
- if prcs is True:
- if optimize is True:
- iPOL = prev.isOnLineSegment(nxt, pnt)
- if iPOL is True:
- onLine = True
- else:
- onLine = False
- output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed}))
- else:
- output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed}))
-
- # Rotate point data
- if onLine is False:
- prev = pnt
- pnt = nxt
- # Efor
-
- temp = PNTS.pop() # Remove temp end point
-
- return output
-
- def _stepTransitionCmds(self, obj, lstPnt, first, minSTH, tolrnc):
- cmds = list()
- rtpd = False
- horizGC = 'G0'
- hSpeed = self.horizRapid
- height = obj.SafeHeight.Value
-
- if obj.CutPattern in ['Line', 'Circular']:
- if obj.OptimizeStepOverTransitions is True:
- height = minSTH + 2.0
- # if obj.LayerMode == 'Multi-pass':
- # rtpd = minSTH
- elif obj.CutPattern in ['ZigZag', 'CircularZigZag']:
- if obj.OptimizeStepOverTransitions is True:
- zChng = first.z - lstPnt.z
- # PathLog.debug('first.z: {}'.format(first.z))
- # PathLog.debug('lstPnt.z: {}'.format(lstPnt.z))
- # PathLog.debug('zChng: {}'.format(zChng))
- # PathLog.debug('minSTH: {}'.format(minSTH))
- if abs(zChng) < tolrnc: # transitions to same Z height
- PathLog.debug('abs(zChng) < tolrnc')
- if (minSTH - first.z) > tolrnc:
- PathLog.debug('(minSTH - first.z) > tolrnc')
- height = minSTH + 2.0
- else:
- PathLog.debug('ELSE (minSTH - first.z) > tolrnc')
- horizGC = 'G1'
- height = first.z
- elif (minSTH + (2.0 * tolrnc)) >= max(first.z, lstPnt.z):
- height = False # allow end of Zig to cut to beginning of Zag
-
-
- # Create raise, shift, and optional lower commands
- if height is not False:
- cmds.append(Path.Command('G0', {'Z': height, 'F': self.vertRapid}))
- cmds.append(Path.Command(horizGC, {'X': first.x, 'Y': first.y, 'F': hSpeed}))
- if rtpd is not False: # ReturnToPreviousDepth
- cmds.append(Path.Command('G0', {'Z': rtpd, 'F': self.vertRapid}))
-
- return cmds
-
- def _breakCmds(self, obj, lstPnt, first, minSTH, tolrnc):
- cmds = list()
- rtpd = False
- horizGC = 'G0'
- hSpeed = self.horizRapid
- height = obj.SafeHeight.Value
-
- if obj.CutPattern in ['Line', 'Circular']:
- if obj.OptimizeStepOverTransitions is True:
- height = minSTH + 2.0
- elif obj.CutPattern in ['ZigZag', 'CircularZigZag']:
- if obj.OptimizeStepOverTransitions is True:
- zChng = first.z - lstPnt.z
- if abs(zChng) < tolrnc: # transitions to same Z height
- if (minSTH - first.z) > tolrnc:
- height = minSTH + 2.0
- else:
- height = first.z + 2.0 # first.z
-
- cmds.append(Path.Command('G0', {'Z': height, 'F': self.vertRapid}))
- cmds.append(Path.Command(horizGC, {'X': first.x, 'Y': first.y, 'F': hSpeed}))
- if rtpd is not False: # ReturnToPreviousDepth
- cmds.append(Path.Command('G0', {'Z': rtpd, 'F': self.vertRapid}))
-
- return cmds
-
- def _arcsToG2G3(self, LN, numPts, odd, gDIR, tolrnc):
- cmds = list()
- strtPnt = LN[0]
- endPnt = LN[numPts - 1]
- strtHght = strtPnt.z
- coPlanar = True
- isCircle = False
- inrPnt = None
- gdi = 0
- if odd is True:
- gdi = 1
-
- # Test if pnt set is circle
- if abs(strtPnt.x - endPnt.x) < tolrnc:
- if abs(strtPnt.y - endPnt.y) < tolrnc:
- if abs(strtPnt.z - endPnt.z) < tolrnc:
- isCircle = True
- isCircle = False
-
- if isCircle is True:
- # convert LN to G2/G3 arc, consolidating GCode
- # https://wiki.shapeoko.com/index.php/G-Code#G2_-_clockwise_arc
- # https://www.cnccookbook.com/cnc-g-code-arc-circle-g02-g03/
- # Dividing circle into two arcs allows for G2/G3 on inclined surfaces
-
- # ijk = self.tmpCOM - strtPnt # vector from start to center
- ijk = self.tmpCOM - strtPnt # vector from start to center
- xyz = self.tmpCOM.add(ijk) # end point
- cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed}))
- cmds.append(Path.Command(gDIR[gdi], {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z,
- 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height
- 'F': self.horizFeed}))
- cmds.append(Path.Command('G1', {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z, 'F': self.horizFeed}))
- ijk = self.tmpCOM - xyz # vector from start to center
- rst = strtPnt # end point
- cmds.append(Path.Command(gDIR[gdi], {'X': rst.x, 'Y': rst.y, 'Z': rst.z,
- 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height
- 'F': self.horizFeed}))
- cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed}))
- else:
- for pt in LN:
- if abs(pt.z - strtHght) > tolrnc: # test for horizontal coplanar
- coPlanar = False
- break
- if coPlanar is True:
- # ijk = self.tmpCOM - strtPnt
- ijk = self.tmpCOM.sub(strtPnt) # vector from start to center
- xyz = endPnt
- cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed}))
- cmds.append(Path.Command(gDIR[gdi], {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z,
- 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height
- 'F': self.horizFeed}))
- cmds.append(Path.Command('G1', {'X': endPnt.x, 'Y': endPnt.y, 'Z': endPnt.z, 'F': self.horizFeed}))
-
- return (coPlanar, cmds)
-
- def _planarApplyDepthOffset(self, SCANDATA, DepthOffset):
- PathLog.debug('Applying DepthOffset value: {}'.format(DepthOffset))
- lenScans = len(SCANDATA)
- for s in range(0, lenScans):
- SO = SCANDATA[s] # StepOver
- numParts = len(SO)
- for prt in range(0, numParts):
- PRT = SO[prt]
- if PRT != 'BRK':
- numPts = len(PRT)
- for pt in range(0, numPts):
- SCANDATA[s][prt][pt].z += DepthOffset
-
- def _planarGetPDC(self, stl, finalDep, SampleInterval, useSafeCutter=False):
- pdc = ocl.PathDropCutter() # create a pdc [PathDropCutter] object
- pdc.setSTL(stl) # add stl model
- if useSafeCutter is True:
- pdc.setCutter(self.safeCutter) # add safeCutter
- else:
- pdc.setCutter(self.cutter) # add cutter
- pdc.setZ(finalDep) # set minimumZ (final / target depth value)
- pdc.setSampling(SampleInterval) # set sampling size
- return pdc
-
- # Main rotational scan functions
- def _processRotationalOp(self, JOB, obj, mdlIdx, compoundFaces=None):
- PathLog.debug('_processRotationalOp(self, obj, mdlIdx, compoundFaces=None)')
- initIdx = 0.0
- final = list()
-
- JOB = PathUtils.findParentJob(obj)
- base = JOB.Model.Group[mdlIdx]
- bb = self.boundBoxes[mdlIdx]
- stl = self.modelSTLs[mdlIdx]
-
- # Rotate model to initial index
- initIdx = obj.CutterTilt + obj.StartIndex
- if initIdx != 0.0:
- self.basePlacement = FreeCAD.ActiveDocument.getObject(base.Name).Placement
- if obj.RotationAxis == 'X':
- base.Placement = FreeCAD.Placement(FreeCAD.Vector(0.0, 0.0, 0.0), FreeCAD.Rotation(FreeCAD.Vector(1.0, 0.0, 0.0), initIdx))
- else:
- base.Placement = FreeCAD.Placement(FreeCAD.Vector(0.0, 0.0, 0.0), FreeCAD.Rotation(FreeCAD.Vector(0.0, 1.0, 0.0), initIdx))
-
- # Prepare global holdpoint container
- if self.holdPoint is None:
- self.holdPoint = ocl.Point(float("inf"), float("inf"), float("inf"))
- if self.layerEndPnt is None:
- self.layerEndPnt = ocl.Point(float("inf"), float("inf"), float("inf"))
-
- # Avoid division by zero in rotational scan calculations
- if obj.FinalDepth.Value <= 0.0:
- zero = obj.SampleInterval.Value # 0.00001
- self.FinalDepth = zero
- obj.FinalDepth.Value = 0.0
- else:
- self.FinalDepth = obj.FinalDepth.Value
-
- # Determine boundbox radius based upon xzy limits data
- if math.fabs(bb.ZMin) > math.fabs(bb.ZMax):
- vlim = bb.ZMin
- else:
- vlim = bb.ZMax
- if obj.RotationAxis == 'X':
- # Rotation is around X-axis, cutter moves along same axis
- if math.fabs(bb.YMin) > math.fabs(bb.YMax):
- hlim = bb.YMin
- else:
- hlim = bb.YMax
- else:
- # Rotation is around Y-axis, cutter moves along same axis
- if math.fabs(bb.XMin) > math.fabs(bb.XMax):
- hlim = bb.XMin
- else:
- hlim = bb.XMax
-
- # Compute max radius of stock, as it rotates, and rotational clearance & safe heights
- self.bbRadius = math.sqrt(hlim**2 + vlim**2)
- self.clearHeight = self.bbRadius + JOB.SetupSheet.ClearanceHeightOffset.Value
- self.safeHeight = self.bbRadius + JOB.SetupSheet.ClearanceHeightOffset.Value
-
- final = self._rotationalDropCutterOp(obj, stl, bb)
-
- return final
-
- def _rotationalDropCutterOp(self, obj, stl, bb):
- self.resetTolerance = 0.0000001 # degrees
- self.layerEndzMax = 0.0
- commands = []
- scanLines = []
- advances = []
- iSTG = []
- rSTG = []
- rings = []
- lCnt = 0
- rNum = 0
- # stepDeg = 1.1
- # layCircum = 1.1
- # begIdx = 0.0
- # endIdx = 0.0
- # arc = 0.0
- # sumAdv = 0.0
- bbRad = self.bbRadius
-
- def invertAdvances(advances):
- idxs = [1.1]
- for adv in advances:
- idxs.append(-1 * adv)
- idxs.pop(0)
- return idxs
-
- def linesToPointRings(scanLines):
- rngs = []
- numPnts = len(scanLines[0]) # Number of points per line along axis, at obj.SampleInterval.Value spacing
- for line in scanLines: # extract circular set(ring) of points from scan lines
- if len(line) != numPnts:
- PathLog.debug('Error: line lengths not equal')
- return rngs
-
- for num in range(0, numPnts):
- rngs.append([1.1]) # Initiate new ring
- for line in scanLines: # extract circular set(ring) of points from scan lines
- rngs[num].append(line[num])
- rngs[num].pop(0)
- return rngs
-
- def indexAdvances(arc, stepDeg):
- indexes = [0.0]
- numSteps = int(math.floor(arc / stepDeg))
- for ns in range(0, numSteps):
- indexes.append(stepDeg)
-
- travel = sum(indexes)
- if arc == 360.0:
- indexes.insert(0, 0.0)
- else:
- indexes.append(arc - travel)
-
- return indexes
-
- # Compute number and size of stepdowns, and final depth
- if obj.LayerMode == 'Single-pass':
- depthparams = [self.FinalDepth]
- else:
- dep_par = PathUtils.depth_params(self.clearHeight, self.safeHeight, self.bbRadius, obj.StepDown.Value, 0.0, self.FinalDepth)
- depthparams = [i for i in dep_par]
- prevDepth = depthparams[0]
- lenDP = len(depthparams)
-
- # Set drop cutter extra offset
- cdeoX = obj.DropCutterExtraOffset.x
- cdeoY = obj.DropCutterExtraOffset.y
-
- # Set updated bound box values and redefine the new min/mas XY area of the operation based on greatest point radius of model
- bb.ZMin = -1 * bbRad
- bb.ZMax = bbRad
- if obj.RotationAxis == 'X':
- bb.YMin = -1 * bbRad
- bb.YMax = bbRad
- ymin = 0.0
- ymax = 0.0
- xmin = bb.XMin - cdeoX
- xmax = bb.XMax + cdeoX
- else:
- bb.XMin = -1 * bbRad
- bb.XMax = bbRad
- ymin = bb.YMin - cdeoY
- ymax = bb.YMax + cdeoY
- xmin = 0.0
- xmax = 0.0
-
- # Calculate arc
- begIdx = obj.StartIndex
- endIdx = obj.StopIndex
- if endIdx < begIdx:
- begIdx -= 360.0
- arc = endIdx - begIdx
-
- # Begin gcode operation with raising cutter to safe height
- commands.append(Path.Command('G0', {'Z': self.safeHeight, 'F': self.vertRapid}))
-
- # Complete rotational scans at layer and translate into gcode
- for layDep in depthparams:
- t_before = time.time()
-
- # Compute circumference and step angles for current layer
- layCircum = 2 * math.pi * layDep
- if lenDP == 1:
- layCircum = 2 * math.pi * bbRad
-
- # Set axial feed rates
- self.axialFeed = 360 / layCircum * self.horizFeed
- self.axialRapid = 360 / layCircum * self.horizRapid
-
- # Determine step angle.
- if obj.RotationAxis == obj.DropCutterDir: # Same == indexed
- stepDeg = (self.cutOut / layCircum) * 360.0
- else:
- stepDeg = (obj.SampleInterval.Value / layCircum) * 360.0
-
- # Limit step angle and determine rotational index angles [indexes].
- if stepDeg > 120.0:
- stepDeg = 120.0
- advances = indexAdvances(arc, stepDeg) # Reset for each step down layer
-
- # Perform rotational indexed scans to layer depth
- if obj.RotationAxis == obj.DropCutterDir: # Same == indexed OR parallel
- sample = obj.SampleInterval.Value
- else:
- sample = self.cutOut
- scanLines = self._indexedDropCutScan(obj, stl, advances, xmin, ymin, xmax, ymax, layDep, sample)
-
- # Complete rotation if necessary
- if arc == 360.0:
- advances.append(360.0 - sum(advances))
- advances.pop(0)
- zero = scanLines.pop(0)
- scanLines.append(zero)
-
- # Translate OCL scans into gcode
- if obj.RotationAxis == obj.DropCutterDir: # Same == indexed (cutter runs parallel to axis)
- # Invert advances if RotationAxis == Y
- if obj.RotationAxis == 'Y':
- advances = invertAdvances(advances)
-
- # Translate scan to gcode
- # sumAdv = 0.0
- sumAdv = begIdx
- for sl in range(0, len(scanLines)):
- sumAdv += advances[sl]
- # Translate scan to gcode
- iSTG = self._indexedScanToGcode(obj, sl, scanLines[sl], sumAdv, prevDepth, layDep, lenDP)
- commands.extend(iSTG)
-
- # Add rise to clear height before beginning next index in CutPattern: Line
- # if obj.CutPattern == 'Line':
- # commands.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid}))
-
- # Raise cutter to safe height after each index cut
- commands.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid}))
- # Eol
- else:
- if self.CutClimb is False:
- advances = invertAdvances(advances)
- advances.reverse()
- scanLines.reverse()
-
- # Invert advances if RotationAxis == Y
- if obj.RotationAxis == 'Y':
- advances = invertAdvances(advances)
-
- # Begin gcode operation with raising cutter to safe height
- commands.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid}))
-
- # Convert rotational scans into gcode
- rings = linesToPointRings(scanLines)
- rNum = 0
- for rng in rings:
- rSTG = self._rotationalScanToGcode(obj, rng, rNum, prevDepth, layDep, advances)
- commands.extend(rSTG)
- if arc != 360.0:
- clrZ = self.layerEndzMax + self.SafeHeightOffset
- commands.append(Path.Command('G0', {'Z': clrZ, 'F': self.vertRapid}))
- rNum += 1
- # Eol
-
- # Add rise to clear height before beginning next index in CutPattern: Line
- # if obj.CutPattern == 'Line':
- # commands.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid}))
-
- prevDepth = layDep
- lCnt += 1 # increment layer count
- PathLog.debug("--Layer " + str(lCnt) + ": " + str(len(advances)) + " OCL scans and gcode in " + str(time.time() - t_before) + " s")
- #time.sleep(0.2)
- # Eol
- return commands
-
- def _indexedDropCutScan(self, obj, stl, advances, xmin, ymin, xmax, ymax, layDep, sample):
- cutterOfst = 0.0
- # radsRot = 0.0
- # reset = 0.0
- iCnt = 0
- Lines = []
- result = None
-
- pdc = ocl.PathDropCutter() # create a pdc
- pdc.setCutter(self.cutter)
- pdc.setZ(layDep) # set minimumZ (final / ta9rget depth value)
- pdc.setSampling(sample)
-
- # if self.useTiltCutter == True:
- if obj.CutterTilt != 0.0:
- cutterOfst = layDep * math.sin(math.radians(obj.CutterTilt))
- PathLog.debug("CutterTilt: cutterOfst is " + str(cutterOfst))
-
- sumAdv = 0.0
- for adv in advances:
- sumAdv += adv
- if adv > 0.0:
- # Rotate STL object using OCL method
- radsRot = math.radians(adv)
- if obj.RotationAxis == 'X':
- stl.rotate(radsRot, 0.0, 0.0)
- else:
- stl.rotate(0.0, radsRot, 0.0)
-
- # Set STL after rotation is made
- pdc.setSTL(stl)
-
- # add Line objects to the path in this loop
- if obj.RotationAxis == 'X':
- p1 = ocl.Point(xmin, cutterOfst, 0.0) # start-point of line
- p2 = ocl.Point(xmax, cutterOfst, 0.0) # end-point of line
- else:
- p1 = ocl.Point(cutterOfst, ymin, 0.0) # start-point of line
- p2 = ocl.Point(cutterOfst, ymax, 0.0) # end-point of line
-
- # Create line object
- if obj.RotationAxis == obj.DropCutterDir: # parallel cut
- if obj.CutPattern == 'ZigZag':
- if (iCnt % 2 == 0.0): # even
- lo = ocl.Line(p1, p2)
- else: # odd
- lo = ocl.Line(p2, p1)
- elif obj.CutPattern == 'Line':
- if self.CutClimb is True:
- lo = ocl.Line(p2, p1)
- else:
- lo = ocl.Line(p1, p2)
- else:
- lo = ocl.Line(p1, p2) # line-object
-
- path = ocl.Path() # create an empty path object
- path.append(lo) # add the line to the path
- pdc.setPath(path) # set path
- pdc.run() # run drop-cutter on the path
- result = pdc.getCLPoints()
- Lines.append(result) # request the list of points
-
- iCnt += 1
- # End loop
- # Rotate STL object back to original position using OCL method
- reset = -1 * math.radians(sumAdv - self.resetTolerance)
- if obj.RotationAxis == 'X':
- stl.rotate(reset, 0.0, 0.0)
- else:
- stl.rotate(0.0, reset, 0.0)
- self.resetTolerance = 0.0
-
- return Lines
-
- def _indexedScanToGcode(self, obj, li, CLP, idxAng, prvDep, layerDepth, numDeps):
- # generate the path commands
- output = []
- optimize = obj.OptimizeLinearPaths
- holdCount = 0
- holdStart = False
- holdStop = False
- zMax = prvDep
- lenCLP = len(CLP)
- lastCLP = lenCLP - 1
- prev = ocl.Point(float("inf"), float("inf"), float("inf"))
- nxt = ocl.Point(float("inf"), float("inf"), float("inf"))
- pnt = ocl.Point(float("inf"), float("inf"), float("inf"))
-
- # Create first point
- pnt.x = CLP[0].x
- pnt.y = CLP[0].y
- pnt.z = CLP[0].z + float(obj.DepthOffset.Value)
-
- # Rotate to correct index location
- if obj.RotationAxis == 'X':
- output.append(Path.Command('G0', {'A': idxAng, 'F': self.axialFeed}))
- else:
- output.append(Path.Command('G0', {'B': idxAng, 'F': self.axialFeed}))
-
- if li > 0:
- if pnt.z > self.layerEndPnt.z:
- clrZ = pnt.z + 2.0
- output.append(Path.Command('G1', {'Z': clrZ, 'F': self.vertRapid}))
- else:
- output.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid}))
-
- output.append(Path.Command('G0', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizRapid}))
- output.append(Path.Command('G1', {'Z': pnt.z, 'F': self.vertFeed}))
-
- for i in range(0, lenCLP):
- if i < lastCLP:
- nxt.x = CLP[i + 1].x
- nxt.y = CLP[i + 1].y
- nxt.z = CLP[i + 1].z + float(obj.DepthOffset.Value)
- else:
- optimize = False
-
- # Update zMax values
- if pnt.z > zMax:
- zMax = pnt.z
-
- if obj.LayerMode == 'Multi-pass':
- # if z travels above previous layer, start/continue hold high cycle
- if pnt.z > prvDep and optimize is True:
- if self.onHold is False:
- holdStart = True
- self.onHold = True
-
- if self.onHold is True:
- if holdStart is True:
- # go to current coordinate
- output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed}))
- # Save holdStart coordinate and prvDep values
- self.holdPoint.x = pnt.x
- self.holdPoint.y = pnt.y
- self.holdPoint.z = pnt.z
- holdCount += 1 # Increment hold count
- holdStart = False # cancel holdStart
-
- # hold cutter high until Z value drops below prvDep
- if pnt.z <= prvDep:
- holdStop = True
-
- if holdStop is True:
- # Send hold and current points to
- zMax += 2.0
- for cmd in self.holdStopCmds(obj, zMax, prvDep, pnt, "Hold Stop: in-line"):
- output.append(cmd)
- # reset necessary hold related settings
- zMax = prvDep
- holdStop = False
- self.onHold = False
- self.holdPoint = ocl.Point(float("inf"), float("inf"), float("inf"))
-
- if self.onHold is False:
- if not optimize or not FreeCAD.Vector(prev.x, prev.y, prev.z).isOnLineSegment(FreeCAD.Vector(nxt.x, nxt.y, nxt.z), FreeCAD.Vector(pnt.x, pnt.y, pnt.z)):
- output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed}))
- # elif i == lastCLP:
- # output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed}))
-
- # Rotate point data
- prev.x = pnt.x
- prev.y = pnt.y
- prev.z = pnt.z
- pnt.x = nxt.x
- pnt.y = nxt.y
- pnt.z = nxt.z
- output.append(Path.Command('N (End index angle ' + str(round(idxAng, 4)) + ')', {}))
-
- # Save layer end point for use in transitioning to next layer
- self.layerEndPnt.x = pnt.x
- self.layerEndPnt.y = pnt.y
- self.layerEndPnt.z = pnt.z
-
- return output
-
- def _rotationalScanToGcode(self, obj, RNG, rN, prvDep, layDep, advances):
- '''_rotationalScanToGcode(obj, RNG, rN, prvDep, layDep, advances) ...
- Convert rotational scan data to gcode path commands.'''
- output = []
- nxtAng = 0
- zMax = 0.0
- # prev = ocl.Point(float("inf"), float("inf"), float("inf"))
- nxt = ocl.Point(float("inf"), float("inf"), float("inf"))
- pnt = ocl.Point(float("inf"), float("inf"), float("inf"))
-
- begIdx = obj.StartIndex
- endIdx = obj.StopIndex
- if endIdx < begIdx:
- begIdx -= 360.0
-
- # Rotate to correct index location
- axisOfRot = 'A'
- if obj.RotationAxis == 'Y':
- axisOfRot = 'B'
-
- # Create first point
- ang = 0.0 + obj.CutterTilt
- pnt.x = RNG[0].x
- pnt.y = RNG[0].y
- pnt.z = RNG[0].z + float(obj.DepthOffset.Value)
-
- # Adjust feed rate based on radius/circumference of cutter.
- # Original feed rate based on travel at circumference.
- if rN > 0:
- # if pnt.z > self.layerEndPnt.z:
- if pnt.z >= self.layerEndzMax:
- clrZ = pnt.z + 5.0
- output.append(Path.Command('G1', {'Z': clrZ, 'F': self.vertRapid}))
- else:
- output.append(Path.Command('G1', {'Z': self.clearHeight, 'F': self.vertRapid}))
-
- output.append(Path.Command('G0', {axisOfRot: ang, 'F': self.axialFeed}))
- output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'F': self.axialFeed}))
- output.append(Path.Command('G1', {'Z': pnt.z, 'F': self.axialFeed}))
-
- lenRNG = len(RNG)
- lastIdx = lenRNG - 1
- for i in range(0, lenRNG):
- if i < lastIdx:
- nxtAng = ang + advances[i + 1]
- nxt.x = RNG[i + 1].x
- nxt.y = RNG[i + 1].y
- nxt.z = RNG[i + 1].z + float(obj.DepthOffset.Value)
-
- if pnt.z > zMax:
- zMax = pnt.z
-
- output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, axisOfRot: ang, 'F': self.axialFeed}))
- pnt.x = nxt.x
- pnt.y = nxt.y
- pnt.z = nxt.z
- ang = nxtAng
-
- # Save layer end point for use in transitioning to next layer
- self.layerEndPnt.x = RNG[0].x
- self.layerEndPnt.y = RNG[0].y
- self.layerEndPnt.z = RNG[0].z
- self.layerEndzMax = zMax
-
- # Move cutter to final point
- # output.append(Path.Command('G1', {'X': self.layerEndPnt.x, 'Y': self.layerEndPnt.y, 'Z': self.layerEndPnt.z, axisOfRot: endang, 'F': self.axialFeed}))
-
- return output
-
-
- '''_loopToGcode(obj, layDep, loop) ... Convert set of loop points to Gcode.'''
- # generate the path commands
- output = []
- optimize = obj.OptimizeLinearPaths
-
- prev = ocl.Point(float("inf"), float("inf"), float("inf"))
- nxt = ocl.Point(float("inf"), float("inf"), float("inf"))
- pnt = ocl.Point(float("inf"), float("inf"), float("inf"))
-
- # Create first point
- pnt.x = loop[0].x
- pnt.y = loop[0].y
- pnt.z = layDep
-
- # Position cutter to begin loop
- output.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid}))
- output.append(Path.Command('G0', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizRapid}))
- output.append(Path.Command('G1', {'Z': pnt.z, 'F': self.vertFeed}))
-
- lenCLP = len(loop)
- lastIdx = lenCLP - 1
- # Cycle through each point on loop
- for i in range(0, lenCLP):
- if i < lastIdx:
- nxt.x = loop[i + 1].x
- nxt.y = loop[i + 1].y
- nxt.z = layDep
- else:
- optimize = False
-
- if not optimize or not FreeCAD.Vector(prev.x, prev.y, prev.z).isOnLineSegment(FreeCAD.Vector(nxt.x, nxt.y, nxt.z), FreeCAD.Vector(pnt.x, pnt.y, pnt.z)):
- output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizFeed}))
-
- # Rotate point data
- prev.x = pnt.x
- prev.y = pnt.y
- prev.z = pnt.z
- pnt.x = nxt.x
- pnt.y = nxt.y
- pnt.z = nxt.z
-
- # Save layer end point for use in transitioning to next layer
- self.layerEndPnt.x = pnt.x
- self.layerEndPnt.y = pnt.y
- self.layerEndPnt.z = pnt.z
-
- return output
-
- def holdStopCmds(self, obj, zMax, pd, p2, txt):
- '''holdStopCmds(obj, zMax, pd, p2, txt) ... Gcode commands to be executed at beginning of hold.'''
- cmds = []
- msg = 'N (' + txt + ')'
- cmds.append(Path.Command(msg, {})) # Raise cutter rapid to zMax in line of travel
- cmds.append(Path.Command('G0', {'Z': zMax, 'F': self.vertRapid})) # Raise cutter rapid to zMax in line of travel
- cmds.append(Path.Command('G0', {'X': p2.x, 'Y': p2.y, 'F': self.horizRapid})) # horizontal rapid to current XY coordinate
- if zMax != pd:
- cmds.append(Path.Command('G0', {'Z': pd, 'F': self.vertRapid})) # drop cutter down rapidly to prevDepth depth
- cmds.append(Path.Command('G0', {'Z': p2.z, 'F': self.vertFeed})) # drop cutter down to current Z depth, returning to normal cut path and speed
- return cmds
-
- def resetOpVariables(self, all=True):
- '''resetOpVariables() ... Reset class variables used for instance of operation.'''
- self.holdPoint = None
- self.layerEndPnt = None
- self.onHold = False
- self.SafeHeightOffset = 2.0
- self.ClearHeightOffset = 4.0
- self.layerEndzMax = 0.0
- self.resetTolerance = 0.0
- self.holdPntCnt = 0
- self.bbRadius = 0.0
- self.axialFeed = 0.0
- self.axialRapid = 0.0
- self.FinalDepth = 0.0
- self.clearHeight = 0.0
- self.safeHeight = 0.0
- self.faceZMax = -999999999999.0
- if all is True:
- self.cutter = None
- self.stl = None
- self.fullSTL = None
- self.cutOut = 0.0
- self.radius = 0.0
- self.useTiltCutter = False
- return True
-
- def deleteOpVariables(self, all=True):
- '''deleteOpVariables() ... Reset class variables used for instance of operation.'''
- del self.holdPoint
- del self.layerEndPnt
- del self.onHold
- del self.SafeHeightOffset
- del self.ClearHeightOffset
- del self.layerEndzMax
- del self.resetTolerance
- del self.holdPntCnt
- del self.bbRadius
- del self.axialFeed
- del self.axialRapid
- del self.FinalDepth
- del self.clearHeight
- del self.safeHeight
- del self.faceZMax
- if all is True:
- del self.cutter
- del self.stl
- del self.fullSTL
- del self.cutOut
- del self.radius
- del self.useTiltCutter
- return True
-
- def setOclCutter(self, obj, safe=False):
- ''' setOclCutter(obj) ... Translation function to convert FreeCAD tool definition to OCL formatted tool. '''
- # Set cutter details
- # https://www.freecadweb.org/api/dd/dfe/classPath_1_1Tool.html#details
- diam_1 = float(obj.ToolController.Tool.Diameter)
- lenOfst = obj.ToolController.Tool.LengthOffset if hasattr(obj.ToolController.Tool, 'LengthOffset') else 0
- FR = obj.ToolController.Tool.FlatRadius if hasattr(obj.ToolController.Tool, 'FlatRadius') else 0
- CEH = obj.ToolController.Tool.CuttingEdgeHeight if hasattr(obj.ToolController.Tool, 'CuttingEdgeHeight') else 0
- CEA = obj.ToolController.Tool.CuttingEdgeAngle if hasattr(obj.ToolController.Tool, 'CuttingEdgeAngle') else 0
-
- # Make safeCutter with 2 mm buffer around physical cutter
- if safe is True:
- diam_1 += 4.0
- if FR != 0.0:
- FR += 2.0
-
- PathLog.debug('ToolType: {}'.format(obj.ToolController.Tool.ToolType))
- if obj.ToolController.Tool.ToolType == 'EndMill':
- # Standard End Mill
- return ocl.CylCutter(diam_1, (CEH + lenOfst))
-
- elif obj.ToolController.Tool.ToolType == 'BallEndMill' and FR == 0.0:
- # Standard Ball End Mill
- # OCL -> BallCutter::BallCutter(diameter, length)
- self.useTiltCutter = True
- return ocl.BallCutter(diam_1, (diam_1 / 2 + lenOfst))
-
- elif obj.ToolController.Tool.ToolType == 'BallEndMill' and FR > 0.0:
- # Bull Nose or Corner Radius cutter
- # Reference: https://www.fine-tools.com/halbstabfraeser.html
- # OCL -> BallCutter::BallCutter(diameter, length)
- return ocl.BullCutter(diam_1, FR, (CEH + lenOfst))
-
- elif obj.ToolController.Tool.ToolType == 'Engraver' and FR > 0.0:
- # Bull Nose or Corner Radius cutter
- # Reference: https://www.fine-tools.com/halbstabfraeser.html
- # OCL -> ConeCutter::ConeCutter(diameter, angle, lengthOffset)
- return ocl.ConeCutter(diam_1, (CEA / 2), lenOfst)
-
- elif obj.ToolController.Tool.ToolType == 'ChamferMill':
- # Bull Nose or Corner Radius cutter
- # Reference: https://www.fine-tools.com/halbstabfraeser.html
- # OCL -> ConeCutter::ConeCutter(diameter, angle, lengthOffset)
- return ocl.ConeCutter(diam_1, (CEA / 2), lenOfst)
- else:
- # Default to standard end mill
- PathLog.warning("Defaulting cutter to standard end mill.")
- return ocl.CylCutter(diam_1, (CEH + lenOfst))
-
- # http://www.carbidecutter.net/products/carbide-burr-cone-shape-sm.html
- '''
- # Available FreeCAD cutter types - some still need translation to available OCL cutter classes.
- Drill, CenterDrill, CounterSink, CounterBore, FlyCutter, Reamer, Tap,
- EndMill, SlotCutter, BallEndMill, ChamferMill, CornerRound, Engraver
- '''
- # Adittional problem is with new ToolBit user-defined cutter shapes.
- # Some sort of translation/conversion will have to be defined to make compatible with OCL.
- PathLog.error('Unable to set OCL cutter.')
- return False
-
- def _getMinSafeTravelHeight(self, pdc, p1, p2, minDep=None):
- A = (p1.x, p1.y)
- B = (p2.x, p2.y)
- LINE = self._planarDropCutScan(pdc, A, B)
- zMax = max([obj.z for obj in LINE])
- if minDep is not None:
- if zMax < minDep:
- zMax = minDep
- return zMax
-
-
-def SetupProperties():
- ''' SetupProperties() ... Return list of properties required for operation.'''
- setup = []
- setup.append('AvoidLastX_Faces')
- setup.append('AvoidLastX_InternalFeatures')
- setup.append('BoundBox')
- setup.append('BoundaryAdjustment')
- setup.append('CircularCenterAt')
- setup.append('CircularCenterCustom')
- setup.append('CircularUseG2G3')
- setup.append('InternalFeaturesCut')
- setup.append('InternalFeaturesAdjustment')
- setup.append('CutMode')
- setup.append('CutPattern')
- setup.append('CutPatternAngle')
- setup.append('CutPatternReversed')
- setup.append('CutterTilt')
- setup.append('DepthOffset')
- setup.append('DropCutterDir')
- setup.append('GapSizes')
- setup.append('GapThreshold')
- setup.append('HandleMultipleFeatures')
- setup.append('LayerMode')
- setup.append('OptimizeStepOverTransitions')
- setup.append('ProfileEdges')
- setup.append('BoundaryEnforcement')
- setup.append('RotationAxis')
- setup.append('SampleInterval')
- setup.append('ScanType')
- setup.append('StartIndex')
- setup.append('StartPoint')
- setup.append('StepOver')
- setup.append('StopIndex')
- setup.append('UseStartPoint')
- setup.append('AngularDeflection')
- setup.append('LinearDeflection')
- # For debugging
- setup.append('ShowTempObjects')
- return setup
-
-
-def Create(name, obj=None):
- '''Create(name) ... Creates and returns a Surface operation.'''
- if obj is None:
- obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
- obj.Proxy = ObjectSurface(obj, name)
- return obj
+# -*- coding: utf-8 -*-
+
+# ***************************************************************************
+# * *
+# * Copyright (c) 2016 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 *
+# * *
+# ***************************************************************************
+
+
+from __future__ import print_function
+
+__title__ = "Path Surface Operation"
+__author__ = "sliptonic (Brad Collette)"
+__url__ = "http://www.freecadweb.org"
+__doc__ = "Class and implementation of 3D Surface operation."
+__contributors__ = "russ4262 (Russell Johnson)"
+
+import FreeCAD
+from PySide import QtCore
+
+# OCL must be installed
+try:
+ import ocl
+except ImportError:
+ msg = QtCore.QCoreApplication.translate("PathSurface", "This operation requires OpenCamLib to be installed.")
+ FreeCAD.Console.PrintError(msg + "\n")
+ raise ImportError
+ # import sys
+ # sys.exit(msg)
+
+import Path
+import PathScripts.PathLog as PathLog
+import PathScripts.PathUtils as PathUtils
+import PathScripts.PathOp as PathOp
+import PathScripts.PathSurfaceSupport as PathSurfaceSupport
+import time
+import math
+
+# lazily loaded modules
+from lazy_loader.lazy_loader import LazyLoader
+MeshPart = LazyLoader('MeshPart', globals(), 'MeshPart')
+Part = LazyLoader('Part', globals(), 'Part')
+
+if FreeCAD.GuiUp:
+ import FreeCADGui
+
+PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
+# PathLog.trackModule(PathLog.thisModule())
+
+
+# Qt translation handling
+def translate(context, text, disambig=None):
+ return QtCore.QCoreApplication.translate(context, text, disambig)
+
+
+class ObjectSurface(PathOp.ObjectOp):
+ '''Proxy object for Surfacing operation.'''
+
+ def baseObject(self):
+ '''baseObject() ... returns super of receiver
+ Used to call base implementation in overwritten functions.'''
+ return super(self.__class__, self)
+
+ def opFeatures(self, obj):
+ '''opFeatures(obj) ... return all standard features and edges based geometries'''
+ return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureHeights | PathOp.FeatureStepDown | PathOp.FeatureCoolant | PathOp.FeatureBaseFaces
+
+ def initOperation(self, obj):
+ '''initPocketOp(obj) ... create operation specific properties'''
+ self.initOpProperties(obj)
+
+ # For debugging
+ if PathLog.getLevel(PathLog.thisModule()) != 4:
+ obj.setEditorMode('ShowTempObjects', 2) # hide
+
+ if not hasattr(obj, 'DoNotSetDefaultValues'):
+ self.setEditorProperties(obj)
+
+ def initOpProperties(self, obj, warn=False):
+ '''initOpProperties(obj) ... create operation specific properties'''
+ missing = list()
+
+ for (prtyp, nm, grp, tt) in self.opProperties():
+ if not hasattr(obj, nm):
+ obj.addProperty(prtyp, nm, grp, tt)
+ missing.append(nm)
+ if warn:
+ newPropMsg = translate('PathSurface', 'New property added to') + ' "{}": '.format(obj.Label) + nm + '. '
+ newPropMsg += translate('PathSurface', 'Check its default value.')
+ PathLog.warning(newPropMsg)
+
+ # Set enumeration lists for enumeration properties
+ if len(missing) > 0:
+ ENUMS = self.propertyEnumerations()
+ for n in ENUMS:
+ if n in missing:
+ setattr(obj, n, ENUMS[n])
+
+ self.addedAllProperties = True
+
+ def opProperties(self):
+ '''opProperties(obj) ... Store operation specific properties'''
+
+ return [
+ ("App::PropertyBool", "ShowTempObjects", "Debug",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Show the temporary path construction objects when module is in DEBUG mode.")),
+
+ ("App::PropertyDistance", "AngularDeflection", "Mesh Conversion",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate mesh. Smaller values increase processing time a lot.")),
+ ("App::PropertyDistance", "LinearDeflection", "Mesh Conversion",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate mesh. Smaller values do not increase processing time much.")),
+
+ ("App::PropertyFloat", "CutterTilt", "Rotation",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Stop index(angle) for rotational scan")),
+ ("App::PropertyEnumeration", "DropCutterDir", "Rotation",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Dropcutter lines are created parallel to this axis.")),
+ ("App::PropertyVectorDistance", "DropCutterExtraOffset", "Rotation",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Additional offset to the selected bounding box")),
+ ("App::PropertyEnumeration", "RotationAxis", "Rotation",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "The model will be rotated around this axis.")),
+ ("App::PropertyFloat", "StartIndex", "Rotation",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Start index(angle) for rotational scan")),
+ ("App::PropertyFloat", "StopIndex", "Rotation",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Stop index(angle) for rotational scan")),
+
+ ("App::PropertyEnumeration", "ScanType", "Surface",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Planar: Flat, 3D surface scan. Rotational: 4th-axis rotational scan.")),
+
+ ("App::PropertyInteger", "AvoidLastX_Faces", "Selected Geometry Settings",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Avoid cutting the last 'N' faces in the Base Geometry list of selected faces.")),
+ ("App::PropertyBool", "AvoidLastX_InternalFeatures", "Selected Geometry Settings",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Do not cut internal features on avoided faces.")),
+ ("App::PropertyDistance", "BoundaryAdjustment", "Selected Geometry Settings",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Positive values push the cutter toward, or beyond, the boundary. Negative values retract the cutter away from the boundary.")),
+ ("App::PropertyBool", "BoundaryEnforcement", "Selected Geometry Settings",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "If true, the cutter will remain inside the boundaries of the model or selected face(s).")),
+ ("App::PropertyEnumeration", "HandleMultipleFeatures", "Selected Geometry Settings",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose how to process multiple Base Geometry features.")),
+ ("App::PropertyDistance", "InternalFeaturesAdjustment", "Selected Geometry Settings",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Positive values push the cutter toward, or into, the feature. Negative values retract the cutter away from the feature.")),
+ ("App::PropertyBool", "InternalFeaturesCut", "Selected Geometry Settings",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Ignore internal feature areas within a larger selected face.")),
+
+ ("App::PropertyEnumeration", "BoundBox", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Select the overall boundary for the operation.")),
+ ("App::PropertyEnumeration", "CutMode", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the direction for the cutting tool to engage the material: Climb (ClockWise) or Conventional (CounterClockWise)")),
+ ("App::PropertyEnumeration", "CutPattern", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the geometric clearing pattern to use for the operation.")),
+ ("App::PropertyFloat", "CutPatternAngle", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "The yaw angle used for certain clearing patterns")),
+ ("App::PropertyBool", "CutPatternReversed", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Reverse the cut order of the stepover paths. For circular cut patterns, begin at the outside and work toward the center.")),
+ ("App::PropertyDistance", "DepthOffset", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the Z-axis depth offset from the target surface.")),
+ ("App::PropertyEnumeration", "LayerMode", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Complete the operation in a single pass at depth, or mulitiple passes to final depth.")),
+ ("App::PropertyVectorDistance", "PatternCenterCustom", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the start point for the cut pattern.")),
+ ("App::PropertyEnumeration", "PatternCenterAt", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose location of the center point for starting the cut pattern.")),
+ ("App::PropertyEnumeration", "ProfileEdges", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Profile the edges of the selection.")),
+ ("App::PropertyDistance", "SampleInterval", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the sampling resolution. Smaller values quickly increase processing time.")),
+ ("App::PropertyPercent", "StepOver", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the stepover percentage, based on the tool's diameter.")),
+
+ ("App::PropertyBool", "OptimizeLinearPaths", "Optimization",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable optimization of linear paths (co-linear points). Removes unnecessary co-linear points from G-Code output.")),
+ ("App::PropertyBool", "OptimizeStepOverTransitions", "Optimization",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable separate optimization of transitions between, and breaks within, each step over path.")),
+ ("App::PropertyBool", "CircularUseG2G3", "Optimization",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Convert co-planar arcs to G2/G3 gcode commands for `Circular` and `CircularZigZag` cut patterns.")),
+ ("App::PropertyDistance", "GapThreshold", "Optimization",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Collinear and co-radial artifact gaps that are smaller than this threshold are closed in the path.")),
+ ("App::PropertyString", "GapSizes", "Optimization",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Feedback: three smallest gaps identified in the path geometry.")),
+
+ ("App::PropertyVectorDistance", "StartPoint", "Start Point",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "The custom start point for the path of this operation")),
+ ("App::PropertyBool", "UseStartPoint", "Start Point",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Make True, if specifying a Start Point"))
+ ]
+
+ def propertyEnumerations(self):
+ # Enumeration lists for App::PropertyEnumeration properties
+ return {
+ 'BoundBox': ['BaseBoundBox', 'Stock'],
+ 'PatternCenterAt': ['CenterOfMass', 'CenterOfBoundBox', 'XminYmin', 'Custom'],
+ 'CutMode': ['Conventional', 'Climb'],
+ 'CutPattern': ['Line', 'Circular', 'CircularZigZag', 'Offset', 'Spiral', 'ZigZag'], # Additional goals ['Offset', 'ZigZagOffset', 'Grid', 'Triangle']
+ 'DropCutterDir': ['X', 'Y'],
+ 'HandleMultipleFeatures': ['Collectively', 'Individually'],
+ 'LayerMode': ['Single-pass', 'Multi-pass'],
+ 'ProfileEdges': ['None', 'Only', 'First', 'Last'],
+ 'RotationAxis': ['X', 'Y'],
+ 'ScanType': ['Planar', 'Rotational']
+ }
+
+ def setEditorProperties(self, obj):
+ # Used to hide inputs in properties list
+
+ P0 = R2 = 0 # 0 = show
+ P2 = R0 = 2 # 2 = hide
+ if obj.ScanType == 'Planar':
+ # if obj.CutPattern in ['Line', 'ZigZag']:
+ if obj.CutPattern in ['Circular', 'CircularZigZag', 'Spiral']:
+ P0 = 2
+ P2 = 0
+ elif obj.CutPattern == 'Offset':
+ P0 = 2
+ elif obj.ScanType == 'Rotational':
+ R2 = P0 = P2 = 2
+ R0 = 0
+ obj.setEditorMode('DropCutterDir', R0)
+ obj.setEditorMode('DropCutterExtraOffset', R0)
+ obj.setEditorMode('RotationAxis', R0)
+ obj.setEditorMode('StartIndex', R0)
+ obj.setEditorMode('StopIndex', R0)
+ obj.setEditorMode('CutterTilt', R0)
+ obj.setEditorMode('CutPattern', R2)
+ obj.setEditorMode('CutPatternAngle', P0)
+ obj.setEditorMode('PatternCenterAt', P2)
+ obj.setEditorMode('PatternCenterCustom', P2)
+
+ def onChanged(self, obj, prop):
+ if hasattr(self, 'addedAllProperties'):
+ if self.addedAllProperties is True:
+ if prop == 'ScanType':
+ self.setEditorProperties(obj)
+ if prop == 'CutPattern':
+ self.setEditorProperties(obj)
+
+ def opOnDocumentRestored(self, obj):
+ self.initOpProperties(obj, warn=True)
+
+ if PathLog.getLevel(PathLog.thisModule()) != 4:
+ obj.setEditorMode('ShowTempObjects', 2) # hide
+ else:
+ obj.setEditorMode('ShowTempObjects', 0) # show
+
+ # Repopulate enumerations in case of changes
+ ENUMS = self.propertyEnumerations()
+ for n in ENUMS:
+ restore = False
+ if hasattr(obj, n):
+ val = obj.getPropertyByName(n)
+ restore = True
+ setattr(obj, n, ENUMS[n])
+ if restore:
+ setattr(obj, n, val)
+
+ self.setEditorProperties(obj)
+
+ def opSetDefaultValues(self, obj, job):
+ '''opSetDefaultValues(obj, job) ... initialize defaults'''
+ job = PathUtils.findParentJob(obj)
+
+ obj.OptimizeLinearPaths = True
+ obj.InternalFeaturesCut = True
+ obj.OptimizeStepOverTransitions = False
+ obj.CircularUseG2G3 = False
+ obj.BoundaryEnforcement = True
+ obj.UseStartPoint = False
+ obj.AvoidLastX_InternalFeatures = True
+ obj.CutPatternReversed = False
+ obj.StartPoint.x = 0.0
+ obj.StartPoint.y = 0.0
+ obj.StartPoint.z = obj.ClearanceHeight.Value
+ obj.ProfileEdges = 'None'
+ obj.LayerMode = 'Single-pass'
+ obj.ScanType = 'Planar'
+ obj.RotationAxis = 'X'
+ obj.CutMode = 'Conventional'
+ obj.CutPattern = 'Line'
+ obj.HandleMultipleFeatures = 'Collectively' # 'Individually'
+ obj.PatternCenterAt = 'CenterOfMass' # 'CenterOfBoundBox', 'XminYmin', 'Custom'
+ obj.GapSizes = 'No gaps identified.'
+ obj.StepOver = 100
+ obj.CutPatternAngle = 0.0
+ obj.CutterTilt = 0.0
+ obj.StartIndex = 0.0
+ obj.StopIndex = 360.0
+ obj.SampleInterval.Value = 1.0
+ obj.BoundaryAdjustment.Value = 0.0
+ obj.InternalFeaturesAdjustment.Value = 0.0
+ obj.AvoidLastX_Faces = 0
+ obj.PatternCenterCustom.x = 0.0
+ obj.PatternCenterCustom.y = 0.0
+ obj.PatternCenterCustom.z = 0.0
+ obj.GapThreshold.Value = 0.005
+ obj.AngularDeflection.Value = 0.25
+ obj.LinearDeflection.Value = job.GeometryTolerance.Value
+ # For debugging
+ obj.ShowTempObjects = False
+
+ if job.GeometryTolerance.Value == 0.0:
+ PathLog.warning(translate('PathSurface', 'The GeometryTolerance for this Job is 0.0. Initializing LinearDeflection to 0.0001 mm.'))
+ obj.LinearDeflection.Value = 0.0001
+
+ # need to overwrite the default depth calculations for facing
+ d = None
+ if job:
+ if job.Stock:
+ d = PathUtils.guessDepths(job.Stock.Shape, None)
+ PathLog.debug("job.Stock exists")
+ else:
+ PathLog.debug("job.Stock NOT exist")
+ else:
+ PathLog.debug("job NOT exist")
+
+ if d is not None:
+ obj.OpFinalDepth.Value = d.final_depth
+ obj.OpStartDepth.Value = d.start_depth
+ else:
+ obj.OpFinalDepth.Value = -10
+ obj.OpStartDepth.Value = 10
+
+ PathLog.debug('Default OpFinalDepth: {}'.format(obj.OpFinalDepth.Value))
+ PathLog.debug('Defualt OpStartDepth: {}'.format(obj.OpStartDepth.Value))
+
+ def opApplyPropertyLimits(self, obj):
+ '''opApplyPropertyLimits(obj) ... Apply necessary limits to user input property values before performing main operation.'''
+ # Limit start index
+ if obj.StartIndex < 0.0:
+ obj.StartIndex = 0.0
+ if obj.StartIndex > 360.0:
+ obj.StartIndex = 360.0
+
+ # Limit stop index
+ if obj.StopIndex > 360.0:
+ obj.StopIndex = 360.0
+ if obj.StopIndex < 0.0:
+ obj.StopIndex = 0.0
+
+ # Limit cutter tilt
+ if obj.CutterTilt < -90.0:
+ obj.CutterTilt = -90.0
+ if obj.CutterTilt > 90.0:
+ obj.CutterTilt = 90.0
+
+ # Limit sample interval
+ if obj.SampleInterval.Value < 0.0001:
+ obj.SampleInterval.Value = 0.0001
+ PathLog.error(translate('PathSurface', 'Sample interval limits are 0.001 to 25.4 millimeters.'))
+ if obj.SampleInterval.Value > 25.4:
+ obj.SampleInterval.Value = 25.4
+ PathLog.error(translate('PathSurface', 'Sample interval limits are 0.001 to 25.4 millimeters.'))
+
+ # Limit cut pattern angle
+ if obj.CutPatternAngle < -360.0:
+ obj.CutPatternAngle = 0.0
+ PathLog.error(translate('PathSurface', 'Cut pattern angle limits are +-360 degrees.'))
+ if obj.CutPatternAngle >= 360.0:
+ obj.CutPatternAngle = 0.0
+ PathLog.error(translate('PathSurface', 'Cut pattern angle limits are +- 360 degrees.'))
+
+ # Limit StepOver to natural number percentage
+ if obj.StepOver > 100:
+ obj.StepOver = 100
+ if obj.StepOver < 1:
+ obj.StepOver = 1
+
+ # Limit AvoidLastX_Faces to zero and positive values
+ if obj.AvoidLastX_Faces < 0:
+ obj.AvoidLastX_Faces = 0
+ PathLog.error(translate('PathSurface', 'AvoidLastX_Faces: Only zero or positive values permitted.'))
+ if obj.AvoidLastX_Faces > 100:
+ obj.AvoidLastX_Faces = 100
+ PathLog.error(translate('PathSurface', 'AvoidLastX_Faces: Avoid last X faces count limited to 100.'))
+
+ def opExecute(self, obj):
+ '''opExecute(obj) ... process surface operation'''
+ PathLog.track()
+
+ self.modelSTLs = list()
+ self.safeSTLs = list()
+ self.modelTypes = list()
+ self.boundBoxes = list()
+ self.profileShapes = list()
+ self.collectiveShapes = list()
+ self.individualShapes = list()
+ self.avoidShapes = list()
+ self.tempGroup = None
+ self.CutClimb = False
+ self.closedGap = False
+ self.tmpCOM = None
+ self.gaps = [0.1, 0.2, 0.3]
+ CMDS = list()
+ modelVisibility = list()
+ FCAD = FreeCAD.ActiveDocument
+
+ try:
+ dotIdx = __name__.index('.') + 1
+ except Exception:
+ dotIdx = 0
+ self.module = __name__[dotIdx:]
+
+ # Set debugging behavior
+ self.showDebugObjects = False # Set to true if you want a visual DocObjects created for some path construction objects
+ self.showDebugObjects = obj.ShowTempObjects
+ deleteTempsFlag = True # Set to False for debugging
+ if PathLog.getLevel(PathLog.thisModule()) == 4:
+ deleteTempsFlag = False
+ else:
+ self.showDebugObjects = False
+
+ # mark beginning of operation and identify parent Job
+ PathLog.info('\nBegin 3D Surface operation...')
+ startTime = time.time()
+
+ # Identify parent Job
+ JOB = PathUtils.findParentJob(obj)
+ self.JOB = JOB
+ if JOB is None:
+ PathLog.error(translate('PathSurface', "No JOB"))
+ return
+ self.stockZMin = JOB.Stock.Shape.BoundBox.ZMin
+
+ # set cut mode; reverse as needed
+ if obj.CutMode == 'Climb':
+ self.CutClimb = True
+ if obj.CutPatternReversed is True:
+ if self.CutClimb is True:
+ self.CutClimb = False
+ else:
+ self.CutClimb = True
+
+ # Begin GCode for operation with basic information
+ # ... and move cutter to clearance height and startpoint
+ output = ''
+ if obj.Comment != '':
+ self.commandlist.append(Path.Command('N ({})'.format(str(obj.Comment)), {}))
+ self.commandlist.append(Path.Command('N ({})'.format(obj.Label), {}))
+ self.commandlist.append(Path.Command('N (Tool type: {})'.format(str(obj.ToolController.Tool.ToolType)), {}))
+ self.commandlist.append(Path.Command('N (Compensated Tool Path. Diameter: {})'.format(str(obj.ToolController.Tool.Diameter)), {}))
+ self.commandlist.append(Path.Command('N (Sample interval: {})'.format(str(obj.SampleInterval.Value)), {}))
+ self.commandlist.append(Path.Command('N (Step over %: {})'.format(str(obj.StepOver)), {}))
+ self.commandlist.append(Path.Command('N ({})'.format(output), {}))
+ self.commandlist.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid}))
+ if obj.UseStartPoint is True:
+ self.commandlist.append(Path.Command('G0', {'X': obj.StartPoint.x, 'Y': obj.StartPoint.y, 'F': self.horizRapid}))
+
+ # Instantiate additional class operation variables
+ self.resetOpVariables()
+
+ # Impose property limits
+ self.opApplyPropertyLimits(obj)
+
+ # Create temporary group for temporary objects, removing existing
+ tempGroupName = 'tempPathSurfaceGroup'
+ if FCAD.getObject(tempGroupName):
+ for to in FCAD.getObject(tempGroupName).Group:
+ FCAD.removeObject(to.Name)
+ FCAD.removeObject(tempGroupName) # remove temp directory if already exists
+ if FCAD.getObject(tempGroupName + '001'):
+ for to in FCAD.getObject(tempGroupName + '001').Group:
+ FCAD.removeObject(to.Name)
+ FCAD.removeObject(tempGroupName + '001') # remove temp directory if already exists
+ tempGroup = FCAD.addObject('App::DocumentObjectGroup', tempGroupName)
+ tempGroupName = tempGroup.Name
+ self.tempGroup = tempGroup
+ tempGroup.purgeTouched()
+ # Add temp object to temp group folder with following code:
+ # ... self.tempGroup.addObject(OBJ)
+
+ # Setup cutter for OCL and cutout value for operation - based on tool controller properties
+ self.cutter = self.setOclCutter(obj)
+ if self.cutter is False:
+ PathLog.error(translate('PathSurface', "Canceling 3D Surface operation. Error creating OCL cutter."))
+ return
+ self.toolDiam = self.cutter.getDiameter()
+ self.radius = self.toolDiam / 2.0
+ self.cutOut = (self.toolDiam * (float(obj.StepOver) / 100.0))
+ self.gaps = [self.toolDiam, self.toolDiam, self.toolDiam]
+
+ # Get height offset values for later use
+ self.SafeHeightOffset = JOB.SetupSheet.SafeHeightOffset.Value
+ self.ClearHeightOffset = JOB.SetupSheet.ClearanceHeightOffset.Value
+
+ # Calculate default depthparams for operation
+ self.depthParams = PathUtils.depth_params(obj.ClearanceHeight.Value, obj.SafeHeight.Value, obj.StartDepth.Value, obj.StepDown.Value, 0.0, obj.FinalDepth.Value)
+ self.midDep = (obj.StartDepth.Value + obj.FinalDepth.Value) / 2.0
+
+ # Save model visibilities for restoration
+ if FreeCAD.GuiUp:
+ for m in range(0, len(JOB.Model.Group)):
+ mNm = JOB.Model.Group[m].Name
+ modelVisibility.append(FreeCADGui.ActiveDocument.getObject(mNm).Visibility)
+
+ # Setup STL, model type, and bound box containers for each model in Job
+ for m in range(0, len(JOB.Model.Group)):
+ M = JOB.Model.Group[m]
+ self.modelSTLs.append(False)
+ self.safeSTLs.append(False)
+ self.profileShapes.append(False)
+ # Set bound box
+ if obj.BoundBox == 'BaseBoundBox':
+ if M.TypeId.startswith('Mesh'):
+ self.modelTypes.append('M') # Mesh
+ self.boundBoxes.append(M.Mesh.BoundBox)
+ else:
+ self.modelTypes.append('S') # Solid
+ self.boundBoxes.append(M.Shape.BoundBox)
+ elif obj.BoundBox == 'Stock':
+ self.modelTypes.append('S') # Solid
+ self.boundBoxes.append(JOB.Stock.Shape.BoundBox)
+
+ # ###### MAIN COMMANDS FOR OPERATION ######
+
+ # Begin processing obj.Base data and creating GCode
+ PSF = PathSurfaceSupport.ProcessSelectedFaces(JOB, obj)
+ PSF.setShowDebugObjects(tempGroup, self.showDebugObjects)
+ PSF.radius = self.radius
+ PSF.depthParams = self.depthParams
+ pPM = PSF.preProcessModel(self.module)
+ # Process selected faces, if available
+ if pPM is False:
+ PathLog.error('Unable to pre-process obj.Base.')
+ else:
+ (FACES, VOIDS) = pPM
+ self.modelSTLs = PSF.modelSTLs
+ self.profileShapes = PSF.profileShapes
+
+ # Create OCL.stl model objects
+ self._prepareModelSTLs(JOB, obj)
+
+ for m in range(0, len(JOB.Model.Group)):
+ Mdl = JOB.Model.Group[m]
+ if FACES[m] is False:
+ PathLog.error('No data for model base: {}'.format(JOB.Model.Group[m].Label))
+ else:
+ if m > 0:
+ # Raise to clearance between models
+ CMDS.append(Path.Command('N (Transition to base: {}.)'.format(Mdl.Label)))
+ CMDS.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid}))
+ PathLog.info('Working on Model.Group[{}]: {}'.format(m, Mdl.Label))
+ # make stock-model-voidShapes STL model for avoidance detection on transitions
+ self._makeSafeSTL(JOB, obj, m, FACES[m], VOIDS[m])
+ # Process model/faces - OCL objects must be ready
+ CMDS.extend(self._processCutAreas(JOB, obj, m, FACES[m], VOIDS[m]))
+
+ # Save gcode produced
+ self.commandlist.extend(CMDS)
+
+ # ###### CLOSING COMMANDS FOR OPERATION ######
+
+ # Delete temporary objects
+ # Restore model visibilities for restoration
+ if FreeCAD.GuiUp:
+ FreeCADGui.ActiveDocument.getObject(tempGroupName).Visibility = False
+ for m in range(0, len(JOB.Model.Group)):
+ M = JOB.Model.Group[m]
+ M.Visibility = modelVisibility[m]
+
+ if deleteTempsFlag is True:
+ for to in tempGroup.Group:
+ if hasattr(to, 'Group'):
+ for go in to.Group:
+ FCAD.removeObject(go.Name)
+ FCAD.removeObject(to.Name)
+ FCAD.removeObject(tempGroupName)
+ else:
+ if len(tempGroup.Group) == 0:
+ FCAD.removeObject(tempGroupName)
+ else:
+ tempGroup.purgeTouched()
+
+ # Provide user feedback for gap sizes
+ gaps = list()
+ for g in self.gaps:
+ if g != self.toolDiam:
+ gaps.append(g)
+ if len(gaps) > 0:
+ obj.GapSizes = '{} mm'.format(gaps)
+ else:
+ if self.closedGap is True:
+ obj.GapSizes = 'Closed gaps < Gap Threshold.'
+ else:
+ obj.GapSizes = 'No gaps identified.'
+
+ # clean up class variables
+ self.resetOpVariables()
+ self.deleteOpVariables()
+
+ self.modelSTLs = None
+ self.safeSTLs = None
+ self.modelTypes = None
+ self.boundBoxes = None
+ self.gaps = None
+ self.closedGap = None
+ self.SafeHeightOffset = None
+ self.ClearHeightOffset = None
+ self.depthParams = None
+ self.midDep = None
+ del self.modelSTLs
+ del self.safeSTLs
+ del self.modelTypes
+ del self.boundBoxes
+ del self.gaps
+ del self.closedGap
+ del self.SafeHeightOffset
+ del self.ClearHeightOffset
+ del self.depthParams
+ del self.midDep
+
+ execTime = time.time() - startTime
+ PathLog.info('Operation time: {} sec.'.format(execTime))
+
+ return True
+
+ # Methods for constructing the cut area
+ def _prepareModelSTLs(self, JOB, obj):
+ PathLog.debug('_prepareModelSTLs()')
+ for m in range(0, len(JOB.Model.Group)):
+ M = JOB.Model.Group[m]
+
+ # PathLog.debug(f" -self.modelTypes[{m}] == 'M'")
+ if self.modelTypes[m] == 'M':
+ # TODO: test if this works
+ facets = M.Mesh.Facets.Points
+ else:
+ facets = Part.getFacets(M.Shape)
+
+ if self.modelSTLs[m] is True:
+ stl = ocl.STLSurf()
+
+ for tri in facets:
+ t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2]),
+ ocl.Point(tri[1][0], tri[1][1], tri[1][2]),
+ ocl.Point(tri[2][0], tri[2][1], tri[2][2]))
+ stl.addTriangle(t)
+ self.modelSTLs[m] = stl
+ return
+
+ def _makeSafeSTL(self, JOB, obj, mdlIdx, faceShapes, voidShapes):
+ '''_makeSafeSTL(JOB, obj, mdlIdx, faceShapes, voidShapes)...
+ Creates and OCL.stl object with combined data with waste stock,
+ model, and avoided faces. Travel lines can be checked against this
+ STL object to determine minimum travel height to clear stock and model.'''
+ PathLog.debug('_makeSafeSTL()')
+
+ fuseShapes = list()
+ Mdl = JOB.Model.Group[mdlIdx]
+ mBB = Mdl.Shape.BoundBox
+ sBB = JOB.Stock.Shape.BoundBox
+
+ # add Model shape to safeSTL shape
+ fuseShapes.append(Mdl.Shape)
+
+ if obj.BoundBox == 'BaseBoundBox':
+ cont = False
+ extFwd = (sBB.ZLength)
+ zmin = mBB.ZMin
+ zmax = mBB.ZMin + extFwd
+ stpDwn = (zmax - zmin) / 4.0
+ dep_par = PathUtils.depth_params(zmax + 5.0, zmax + 3.0, zmax, stpDwn, 0.0, zmin)
+
+ try:
+ envBB = PathUtils.getEnvelope(partshape=Mdl.Shape, depthparams=dep_par) # Produces .Shape
+ cont = True
+ except Exception as ee:
+ PathLog.error(str(ee))
+ shell = Mdl.Shape.Shells[0]
+ solid = Part.makeSolid(shell)
+ try:
+ envBB = PathUtils.getEnvelope(partshape=solid, depthparams=dep_par) # Produces .Shape
+ cont = True
+ except Exception as eee:
+ PathLog.error(str(eee))
+
+ if cont:
+ stckWst = JOB.Stock.Shape.cut(envBB)
+ if obj.BoundaryAdjustment > 0.0:
+ cmpndFS = Part.makeCompound(faceShapes)
+ baBB = PathUtils.getEnvelope(partshape=cmpndFS, depthparams=self.depthParams) # Produces .Shape
+ adjStckWst = stckWst.cut(baBB)
+ else:
+ adjStckWst = stckWst
+ fuseShapes.append(adjStckWst)
+ else:
+ PathLog.warning('Path transitions might not avoid the model. Verify paths.')
+ else:
+ # If boundbox is Job.Stock, add hidden pad under stock as base plate
+ toolDiam = self.cutter.getDiameter()
+ zMin = JOB.Stock.Shape.BoundBox.ZMin
+ xMin = JOB.Stock.Shape.BoundBox.XMin - toolDiam
+ yMin = JOB.Stock.Shape.BoundBox.YMin - toolDiam
+ bL = JOB.Stock.Shape.BoundBox.XLength + (2 * toolDiam)
+ bW = JOB.Stock.Shape.BoundBox.YLength + (2 * toolDiam)
+ bH = 1.0
+ crnr = FreeCAD.Vector(xMin, yMin, zMin - 1.0)
+ B = Part.makeBox(bL, bW, bH, crnr, FreeCAD.Vector(0, 0, 1))
+ fuseShapes.append(B)
+
+ if voidShapes is not False:
+ voidComp = Part.makeCompound(voidShapes)
+ voidEnv = PathUtils.getEnvelope(partshape=voidComp, depthparams=self.depthParams) # Produces .Shape
+ fuseShapes.append(voidEnv)
+
+ fused = Part.makeCompound(fuseShapes)
+
+ if self.showDebugObjects:
+ T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'safeSTLShape')
+ T.Shape = fused
+ T.purgeTouched()
+ self.tempGroup.addObject(T)
+
+ facets = Part.getFacets(fused)
+
+ stl = ocl.STLSurf()
+ for tri in facets:
+ t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2]),
+ ocl.Point(tri[1][0], tri[1][1], tri[1][2]),
+ ocl.Point(tri[2][0], tri[2][1], tri[2][2]))
+ stl.addTriangle(t)
+
+ self.safeSTLs[mdlIdx] = stl
+
+ def _processCutAreas(self, JOB, obj, mdlIdx, FCS, VDS):
+ '''_processCutAreas(JOB, obj, mdlIdx, FCS, VDS)...
+ This method applies any avoided faces or regions to the selected faces.
+ It then calls the correct scan method depending on the ScanType property.'''
+ PathLog.debug('_processCutAreas()')
+
+ final = list()
+
+ # Process faces Collectively or Individually
+ if obj.HandleMultipleFeatures == 'Collectively':
+ if FCS is True:
+ COMP = False
+ else:
+ ADD = Part.makeCompound(FCS)
+ if VDS is not False:
+ DEL = Part.makeCompound(VDS)
+ COMP = ADD.cut(DEL)
+ else:
+ COMP = ADD
+
+ if obj.ScanType == 'Planar':
+ final.extend(self._processPlanarOp(JOB, obj, mdlIdx, COMP, 0))
+ elif obj.ScanType == 'Rotational':
+ final.extend(self._processRotationalOp(JOB, obj, mdlIdx, COMP))
+
+ elif obj.HandleMultipleFeatures == 'Individually':
+ for fsi in range(0, len(FCS)):
+ fShp = FCS[fsi]
+ # self.deleteOpVariables(all=False)
+ self.resetOpVariables(all=False)
+
+ if fShp is True:
+ COMP = False
+ else:
+ ADD = Part.makeCompound([fShp])
+ if VDS is not False:
+ DEL = Part.makeCompound(VDS)
+ COMP = ADD.cut(DEL)
+ else:
+ COMP = ADD
+
+ if obj.ScanType == 'Planar':
+ final.extend(self._processPlanarOp(JOB, obj, mdlIdx, COMP, fsi))
+ elif obj.ScanType == 'Rotational':
+ final.extend(self._processRotationalOp(JOB, obj, mdlIdx, COMP))
+ COMP = None
+ # Eif
+
+ return final
+
+ # Methods for creating path geometry
+ def _processPlanarOp(self, JOB, obj, mdlIdx, cmpdShp, fsi):
+ '''_processPlanarOp(JOB, obj, mdlIdx, cmpdShp)...
+ This method compiles the main components for the procedural portion of a planar operation (non-rotational).
+ It creates the OCL PathDropCutter objects: model and safeTravel.
+ It makes the necessary facial geometries for the actual cut area.
+ It calls the correct Single or Multi-pass method as needed.
+ It returns the gcode for the operation. '''
+ PathLog.debug('_processPlanarOp()')
+ final = list()
+ SCANDATA = list()
+
+ def getTransition(two):
+ first = two[0][0][0] # [step][item][point]
+ safe = obj.SafeHeight.Value + 0.1
+ trans = [[FreeCAD.Vector(first.x, first.y, safe)]]
+ return trans
+
+ # Compute number and size of stepdowns, and final depth
+ if obj.LayerMode == 'Single-pass':
+ depthparams = [obj.FinalDepth.Value]
+ elif obj.LayerMode == 'Multi-pass':
+ depthparams = [i for i in self.depthParams]
+ lenDP = len(depthparams)
+
+ # Prepare PathDropCutter objects with STL data
+ pdc = self._planarGetPDC(self.modelSTLs[mdlIdx], depthparams[lenDP - 1], obj.SampleInterval.Value, self.cutter)
+ safePDC = self._planarGetPDC(self.safeSTLs[mdlIdx], depthparams[lenDP - 1], obj.SampleInterval.Value, self.cutter)
+
+ profScan = list()
+ if obj.ProfileEdges != 'None':
+ prflShp = self.profileShapes[mdlIdx][fsi]
+ if prflShp is False:
+ PathLog.error('No profile shape is False.')
+ return list()
+ if self.showDebugObjects:
+ P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpNewProfileShape')
+ P.Shape = prflShp
+ P.purgeTouched()
+ self.tempGroup.addObject(P)
+ # get offset path geometry and perform OCL scan with that geometry
+ pathOffsetGeom = self._offsetFacesToPointData(obj, prflShp)
+ if pathOffsetGeom is False:
+ PathLog.error('No profile geometry returned.')
+ return list()
+ profScan = [self._planarPerformOclScan(obj, pdc, pathOffsetGeom, True)]
+
+ geoScan = list()
+ if obj.ProfileEdges != 'Only':
+ if self.showDebugObjects:
+ F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCutArea')
+ F.Shape = cmpdShp
+ F.purgeTouched()
+ self.tempGroup.addObject(F)
+ # get internal path geometry and perform OCL scan with that geometry
+ PGG = PathSurfaceSupport.PathGeometryGenerator(obj, cmpdShp, obj.CutPattern)
+ if self.showDebugObjects:
+ PGG.setDebugObjectsGroup(self.tempGroup)
+ self.tmpCOM = PGG.getCenterOfPattern()
+ pathGeom = PGG.generatePathGeometry()
+ if pathGeom is False:
+ PathLog.error('No path geometry returned.')
+ return list()
+ if obj.CutPattern == 'Offset':
+ useGeom = self._offsetFacesToPointData(obj, pathGeom, profile=False)
+ if useGeom is False:
+ PathLog.error('No profile geometry returned.')
+ return list()
+ geoScan = [self._planarPerformOclScan(obj, pdc, useGeom, True)]
+ else:
+ geoScan = self._planarPerformOclScan(obj, pdc, pathGeom, False)
+
+ if obj.ProfileEdges == 'Only': # ['None', 'Only', 'First', 'Last']
+ SCANDATA.extend(profScan)
+ if obj.ProfileEdges == 'None':
+ SCANDATA.extend(geoScan)
+ if obj.ProfileEdges == 'First':
+ profScan.append(getTransition(geoScan))
+ SCANDATA.extend(profScan)
+ SCANDATA.extend(geoScan)
+ if obj.ProfileEdges == 'Last':
+ SCANDATA.extend(geoScan)
+ SCANDATA.extend(profScan)
+
+ if len(SCANDATA) == 0:
+ PathLog.error('No scan data to convert to Gcode.')
+ return list()
+
+ # Apply depth offset
+ if obj.DepthOffset.Value != 0.0:
+ self._planarApplyDepthOffset(SCANDATA, obj.DepthOffset.Value)
+
+ # If cut pattern is `Circular`, there are zero(almost zero) straight lines to optimize
+ # Store initial `OptimizeLinearPaths` value for later restoration
+ self.preOLP = obj.OptimizeLinearPaths
+ if obj.CutPattern in ['Circular', 'CircularZigZag']:
+ obj.OptimizeLinearPaths = False
+
+ # Process OCL scan data
+ if obj.LayerMode == 'Single-pass':
+ final.extend(self._planarDropCutSingle(JOB, obj, pdc, safePDC, depthparams, SCANDATA))
+ elif obj.LayerMode == 'Multi-pass':
+ final.extend(self._planarDropCutMulti(JOB, obj, pdc, safePDC, depthparams, SCANDATA))
+
+ # If cut pattern is `Circular`, restore initial OLP value
+ if obj.CutPattern in ['Circular', 'CircularZigZag']:
+ obj.OptimizeLinearPaths = self.preOLP
+
+ # Raise to safe height between individual faces.
+ if obj.HandleMultipleFeatures == 'Individually':
+ final.insert(0, Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
+
+ return final
+
+ def _offsetFacesToPointData(self, obj, subShp, profile=True):
+ PathLog.debug('_offsetFacesToPointData()')
+
+ offsetLists = list()
+ dist = obj.SampleInterval.Value / 5.0
+ # defl = obj.SampleInterval.Value / 5.0
+
+ if not profile:
+ # Reverse order of wires in each face - inside to outside
+ for w in range(len(subShp.Wires) - 1, -1, -1):
+ W = subShp.Wires[w]
+ PNTS = W.discretize(Distance=dist)
+ # PNTS = W.discretize(Deflection=defl)
+ if self.CutClimb:
+ PNTS.reverse()
+ offsetLists.append(PNTS)
+ else:
+ # Reference https://forum.freecadweb.org/viewtopic.php?t=28861#p234939
+ for fc in subShp.Faces:
+ # Reverse order of wires in each face - inside to outside
+ for w in range(len(fc.Wires) - 1, -1, -1):
+ W = fc.Wires[w]
+ PNTS = W.discretize(Distance=dist)
+ # PNTS = W.discretize(Deflection=defl)
+ if self.CutClimb:
+ PNTS.reverse()
+ offsetLists.append(PNTS)
+
+ return offsetLists
+
+ def _planarPerformOclScan(self, obj, pdc, pathGeom, offsetPoints=False):
+ '''_planarPerformOclScan(obj, pdc, pathGeom, offsetPoints=False)...
+ Switching function for calling the appropriate path-geometry to OCL points conversion function
+ for the various cut patterns.'''
+ PathLog.debug('_planarPerformOclScan()')
+ SCANS = list()
+
+ if offsetPoints or obj.CutPattern == 'Offset':
+ PNTSET = PathSurfaceSupport.pathGeomToOffsetPointSet(obj, pathGeom)
+ for D in PNTSET:
+ stpOvr = list()
+ ofst = list()
+ for I in D:
+ if I == 'BRK':
+ stpOvr.append(ofst)
+ stpOvr.append(I)
+ ofst = list()
+ else:
+ # D format is ((p1, p2), (p3, p4))
+ (A, B) = I
+ ofst.extend(self._planarDropCutScan(pdc, A, B))
+ if len(ofst) > 0:
+ stpOvr.append(ofst)
+ SCANS.extend(stpOvr)
+ elif obj.CutPattern in ['Line', 'Spiral', 'ZigZag']:
+ stpOvr = list()
+ if obj.CutPattern == 'Line':
+ PNTSET = PathSurfaceSupport.pathGeomToLinesPointSet(obj, pathGeom, self.CutClimb, self.toolDiam, self.closedGap, self.gaps)
+ elif obj.CutPattern == 'ZigZag':
+ PNTSET = PathSurfaceSupport.pathGeomToZigzagPointSet(obj, pathGeom, self.CutClimb, self.toolDiam, self.closedGap, self.gaps)
+ elif obj.CutPattern == 'Spiral':
+ PNTSET = PathSurfaceSupport.pathGeomToSpiralPointSet(obj, pathGeom)
+
+ for STEP in PNTSET:
+ for LN in STEP:
+ if LN == 'BRK':
+ stpOvr.append(LN)
+ else:
+ # D format is ((p1, p2), (p3, p4))
+ (A, B) = LN
+ stpOvr.append(self._planarDropCutScan(pdc, A, B))
+ SCANS.append(stpOvr)
+ stpOvr = list()
+ elif obj.CutPattern in ['Circular', 'CircularZigZag']:
+ # PNTSET is list, by stepover.
+ # Each stepover is a list containing arc/loop descriptions, (sp, ep, cp)
+ PNTSET = PathSurfaceSupport.pathGeomToCircularPointSet(obj, pathGeom, self.CutClimb, self.toolDiam, self.closedGap, self.gaps, self.tmpCOM)
+
+ for so in range(0, len(PNTSET)):
+ stpOvr = list()
+ erFlg = False
+ (aTyp, dirFlg, ARCS) = PNTSET[so]
+
+ if dirFlg == 1: # 1
+ cMode = True
+ else:
+ cMode = False
+
+ for a in range(0, len(ARCS)):
+ Arc = ARCS[a]
+ if Arc == 'BRK':
+ stpOvr.append('BRK')
+ else:
+ scan = self._planarCircularDropCutScan(pdc, Arc, cMode)
+ if scan is False:
+ erFlg = True
+ else:
+ if aTyp == 'L':
+ scan.append(FreeCAD.Vector(scan[0].x, scan[0].y, scan[0].z))
+ stpOvr.append(scan)
+ if erFlg is False:
+ SCANS.append(stpOvr)
+ # Eif
+
+ return SCANS
+
+ def _planarDropCutScan(self, pdc, A, B):
+ #PNTS = list()
+ (x1, y1) = A
+ (x2, y2) = B
+ path = ocl.Path() # create an empty path object
+ p1 = ocl.Point(x1, y1, 0) # start-point of line
+ p2 = ocl.Point(x2, y2, 0) # end-point of line
+ lo = ocl.Line(p1, p2) # line-object
+ path.append(lo) # add the line to the path
+ pdc.setPath(path)
+ pdc.run() # run dropcutter algorithm on path
+ CLP = pdc.getCLPoints()
+ PNTS = [FreeCAD.Vector(p.x, p.y, p.z) for p in CLP]
+ return PNTS # pdc.getCLPoints()
+
+ def _planarCircularDropCutScan(self, pdc, Arc, cMode):
+ PNTS = list()
+ path = ocl.Path() # create an empty path object
+ (sp, ep, cp) = Arc
+
+ # process list of segment tuples (vect, vect)
+ p1 = ocl.Point(sp[0], sp[1], 0) # start point of arc
+ p2 = ocl.Point(ep[0], ep[1], 0) # end point of arc
+ C = ocl.Point(cp[0], cp[1], 0) # center point of arc
+ ao = ocl.Arc(p1, p2, C, cMode) # arc object
+ path.append(ao) # add the arc to the path
+ pdc.setPath(path)
+ pdc.run() # run dropcutter algorithm on path
+ CLP = pdc.getCLPoints()
+
+ # Convert OCL object data to FreeCAD vectors
+ return [FreeCAD.Vector(p.x, p.y, p.z) for p in CLP]
+
+ # Main planar scan functions
+ def _planarDropCutSingle(self, JOB, obj, pdc, safePDC, depthparams, SCANDATA):
+ PathLog.debug('_planarDropCutSingle()')
+
+ GCODE = [Path.Command('N (Beginning of Single-pass layer.)', {})]
+ tolrnc = JOB.GeometryTolerance.Value
+ lenSCANDATA = len(SCANDATA)
+ gDIR = ['G3', 'G2']
+
+ if self.CutClimb:
+ gDIR = ['G2', 'G3']
+
+ # Set `ProfileEdges` specific trigger indexes
+ peIdx = lenSCANDATA # off by default
+ if obj.ProfileEdges == 'Only':
+ peIdx = -1
+ elif obj.ProfileEdges == 'First':
+ peIdx = 0
+ elif obj.ProfileEdges == 'Last':
+ peIdx = lenSCANDATA - 1
+
+ # Send cutter to x,y position of first point on first line
+ first = SCANDATA[0][0][0] # [step][item][point]
+ GCODE.append(Path.Command('G0', {'X': first.x, 'Y': first.y, 'F': self.horizRapid}))
+
+ # Cycle through step-over sections (line segments or arcs)
+ odd = True
+ lstStpEnd = None
+ for so in range(0, lenSCANDATA):
+ cmds = list()
+ PRTS = SCANDATA[so]
+ lenPRTS = len(PRTS)
+ first = PRTS[0][0] # first point of arc/line stepover group
+ start = PRTS[0][0] # will change with each line/arc segment
+ last = None
+ cmds.append(Path.Command('N (Begin step {}.)'.format(so), {}))
+
+ if so > 0:
+ if obj.CutPattern == 'CircularZigZag':
+ if odd:
+ odd = False
+ else:
+ odd = True
+ minTrnsHght = self._getMinSafeTravelHeight(safePDC, lstStpEnd, first) # Check safe travel height against fullSTL
+ # cmds.append(Path.Command('N (Transition: last, first: {}, {}: minSTH: {})'.format(lstStpEnd, first, minTrnsHght), {}))
+ cmds.extend(self._stepTransitionCmds(obj, lstStpEnd, first, minTrnsHght, tolrnc))
+
+ # Override default `OptimizeLinearPaths` behavior to allow `ProfileEdges` optimization
+ if so == peIdx or peIdx == -1:
+ obj.OptimizeLinearPaths = self.preOLP
+
+ # Cycle through current step-over parts
+ for i in range(0, lenPRTS):
+ prt = PRTS[i]
+ lenPrt = len(prt)
+ if prt == 'BRK':
+ nxtStart = PRTS[i + 1][0]
+ minSTH = self._getMinSafeTravelHeight(safePDC, last, nxtStart) # Check safe travel height against fullSTL
+ cmds.append(Path.Command('N (Break)', {}))
+ cmds.extend(self._breakCmds(obj, last, nxtStart, minSTH, tolrnc))
+ else:
+ cmds.append(Path.Command('N (part {}.)'.format(i + 1), {}))
+ start = prt[0]
+ last = prt[lenPrt - 1]
+ if so == peIdx or peIdx == -1:
+ cmds.extend(self._planarSinglepassProcess(obj, prt))
+ elif obj.CutPattern in ['Circular', 'CircularZigZag'] and obj.CircularUseG2G3 is True and lenPrt > 2:
+ (rtnVal, gcode) = self._arcsToG2G3(prt, lenPrt, odd, gDIR, tolrnc)
+ if rtnVal:
+ cmds.extend(gcode)
+ else:
+ cmds.extend(self._planarSinglepassProcess(obj, prt))
+ else:
+ cmds.extend(self._planarSinglepassProcess(obj, prt))
+ cmds.append(Path.Command('N (End of step {}.)'.format(so), {}))
+ GCODE.extend(cmds) # save line commands
+ lstStpEnd = last
+
+ # Return `OptimizeLinearPaths` to disabled
+ if so == peIdx or peIdx == -1:
+ if obj.CutPattern in ['Circular', 'CircularZigZag']:
+ obj.OptimizeLinearPaths = False
+ # Efor
+
+ return GCODE
+
+ def _planarSinglepassProcess(self, obj, PNTS):
+ output = []
+ optimize = obj.OptimizeLinearPaths
+ lenPNTS = len(PNTS)
+ lop = None
+ onLine = False
+
+ # Initialize first three points
+ nxt = None
+ pnt = PNTS[0]
+ prev = FreeCAD.Vector(-442064564.6, 258539656553.27, 3538553425.847)
+
+ # Add temp end point
+ PNTS.append(FreeCAD.Vector(-4895747464.6, -25855763553.2, 35865763425))
+
+ # Begin processing ocl points list into gcode
+ for i in range(0, lenPNTS):
+ # Calculate next point for consideration with current point
+ nxt = PNTS[i + 1]
+
+ # Process point
+ if optimize:
+ if pnt.isOnLineSegment(prev, nxt):
+ onLine = True
+ else:
+ onLine = False
+ output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed}))
+ else:
+ output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed}))
+
+ # Rotate point data
+ if onLine is False:
+ prev = pnt
+ pnt = nxt
+ # Efor
+
+ PNTS.pop() # Remove temp end point
+
+ return output
+
+ def _planarDropCutMulti(self, JOB, obj, pdc, safePDC, depthparams, SCANDATA):
+ GCODE = [Path.Command('N (Beginning of Multi-pass layers.)', {})]
+ tolrnc = JOB.GeometryTolerance.Value
+ lenDP = len(depthparams)
+ prevDepth = depthparams[0]
+ lenSCANDATA = len(SCANDATA)
+ gDIR = ['G3', 'G2']
+
+ if self.CutClimb:
+ gDIR = ['G2', 'G3']
+
+ # Set `ProfileEdges` specific trigger indexes
+ peIdx = lenSCANDATA # off by default
+ if obj.ProfileEdges == 'Only':
+ peIdx = -1
+ elif obj.ProfileEdges == 'First':
+ peIdx = 0
+ elif obj.ProfileEdges == 'Last':
+ peIdx = lenSCANDATA - 1
+
+ # Process each layer in depthparams
+ prvLyrFirst = None
+ prvLyrLast = None
+ lastPrvStpLast = None
+ for lyr in range(0, lenDP):
+ odd = True # ZigZag directional switch
+ lyrHasCmds = False
+ actvSteps = 0
+ LYR = list()
+ prvStpFirst = None
+ if lyr > 0:
+ if prvStpLast is not None:
+ lastPrvStpLast = prvStpLast
+ prvStpLast = None
+ lyrDep = depthparams[lyr]
+ PathLog.debug('Multi-pass lyrDep: {}'.format(round(lyrDep, 4)))
+
+ # Cycle through step-over sections (line segments or arcs)
+ for so in range(0, len(SCANDATA)):
+ SO = SCANDATA[so]
+ lenSO = len(SO)
+
+ # Pre-process step-over parts for layer depth and holds
+ ADJPRTS = list()
+ LMAX = list()
+ soHasPnts = False
+ brkFlg = False
+ for i in range(0, lenSO):
+ prt = SO[i]
+ lenPrt = len(prt)
+ if prt == 'BRK':
+ if brkFlg:
+ ADJPRTS.append(prt)
+ LMAX.append(prt)
+ brkFlg = False
+ else:
+ (PTS, lMax) = self._planarMultipassPreProcess(obj, prt, prevDepth, lyrDep)
+ if len(PTS) > 0:
+ ADJPRTS.append(PTS)
+ soHasPnts = True
+ brkFlg = True
+ LMAX.append(lMax)
+ # Efor
+ lenAdjPrts = len(ADJPRTS)
+
+ # Process existing parts within current step over
+ prtsHasCmds = False
+ stepHasCmds = False
+ prtsCmds = list()
+ stpOvrCmds = list()
+ transCmds = list()
+ if soHasPnts is True:
+ first = ADJPRTS[0][0] # first point of arc/line stepover group
+
+ # Manage step over transition and CircularZigZag direction
+ if so > 0:
+ # PathLog.debug(' stepover index: {}'.format(so))
+ # Control ZigZag direction
+ if obj.CutPattern == 'CircularZigZag':
+ if odd is True:
+ odd = False
+ else:
+ odd = True
+ # Control step over transition
+ if prvStpLast is None:
+ prvStpLast = lastPrvStpLast
+ minTrnsHght = self._getMinSafeTravelHeight(safePDC, prvStpLast, first, minDep=None) # Check safe travel height against fullSTL
+ transCmds.append(Path.Command('N (--Step {} transition)'.format(so), {}))
+ transCmds.extend(self._stepTransitionCmds(obj, prvStpLast, first, minTrnsHght, tolrnc))
+
+ # Override default `OptimizeLinearPaths` behavior to allow `ProfileEdges` optimization
+ if so == peIdx or peIdx == -1:
+ obj.OptimizeLinearPaths = self.preOLP
+
+ # Cycle through current step-over parts
+ for i in range(0, lenAdjPrts):
+ prt = ADJPRTS[i]
+ lenPrt = len(prt)
+ # PathLog.debug(' adj parts index - lenPrt: {} - {}'.format(i, lenPrt))
+ if prt == 'BRK' and prtsHasCmds is True:
+ nxtStart = ADJPRTS[i + 1][0]
+ minSTH = self._getMinSafeTravelHeight(safePDC, last, nxtStart, minDep=None) # Check safe travel height against fullSTL
+ prtsCmds.append(Path.Command('N (--Break)', {}))
+ prtsCmds.extend(self._breakCmds(obj, last, nxtStart, minSTH, tolrnc))
+ else:
+ segCmds = False
+ prtsCmds.append(Path.Command('N (part {})'.format(i + 1), {}))
+ last = prt[lenPrt - 1]
+ if so == peIdx or peIdx == -1:
+ segCmds = self._planarSinglepassProcess(obj, prt)
+ elif obj.CutPattern in ['Circular', 'CircularZigZag'] and obj.CircularUseG2G3 is True and lenPrt > 2:
+ (rtnVal, gcode) = self._arcsToG2G3(prt, lenPrt, odd, gDIR, tolrnc)
+ if rtnVal is True:
+ segCmds = gcode
+ else:
+ segCmds = self._planarSinglepassProcess(obj, prt)
+ else:
+ segCmds = self._planarSinglepassProcess(obj, prt)
+
+ if segCmds is not False:
+ prtsCmds.extend(segCmds)
+ prtsHasCmds = True
+ prvStpLast = last
+ # Eif
+ # Efor
+ # Eif
+
+ # Return `OptimizeLinearPaths` to disabled
+ if so == peIdx or peIdx == -1:
+ if obj.CutPattern in ['Circular', 'CircularZigZag']:
+ obj.OptimizeLinearPaths = False
+
+ # Compile step over(prts) commands
+ if prtsHasCmds is True:
+ stepHasCmds = True
+ actvSteps += 1
+ prvStpFirst = first
+ stpOvrCmds.extend(transCmds)
+ stpOvrCmds.append(Path.Command('N (Begin step {}.)'.format(so), {}))
+ stpOvrCmds.append(Path.Command('G0', {'X': first.x, 'Y': first.y, 'F': self.horizRapid}))
+ stpOvrCmds.extend(prtsCmds)
+ stpOvrCmds.append(Path.Command('N (End of step {}.)'.format(so), {}))
+
+ # Layer transition at first active step over in current layer
+ if actvSteps == 1:
+ prvLyrFirst = first
+ LYR.append(Path.Command('N (Layer {} begins)'.format(lyr), {}))
+ if lyr > 0:
+ LYR.append(Path.Command('N (Layer transition)', {}))
+ LYR.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
+ LYR.append(Path.Command('G0', {'X': first.x, 'Y': first.y, 'F': self.horizRapid}))
+
+ if stepHasCmds is True:
+ lyrHasCmds = True
+ LYR.extend(stpOvrCmds)
+ # Eif
+
+ # Close layer, saving commands, if any
+ if lyrHasCmds is True:
+ prvLyrLast = last
+ GCODE.extend(LYR) # save line commands
+ GCODE.append(Path.Command('N (End of layer {})'.format(lyr), {}))
+
+ # Set previous depth
+ prevDepth = lyrDep
+ # Efor
+
+ PathLog.debug('Multi-pass op has {} layers (step downs).'.format(lyr + 1))
+
+ return GCODE
+
+ def _planarMultipassPreProcess(self, obj, LN, prvDep, layDep):
+ ALL = list()
+ PTS = list()
+ optLinTrans = obj.OptimizeStepOverTransitions
+ safe = math.ceil(obj.SafeHeight.Value)
+
+ if optLinTrans is True:
+ for P in LN:
+ ALL.append(P)
+ # Handle layer depth AND hold points
+ if P.z <= layDep:
+ PTS.append(FreeCAD.Vector(P.x, P.y, layDep))
+ elif P.z > prvDep:
+ PTS.append(FreeCAD.Vector(P.x, P.y, safe))
+ else:
+ PTS.append(FreeCAD.Vector(P.x, P.y, P.z))
+ # Efor
+ else:
+ for P in LN:
+ ALL.append(P)
+ # Handle layer depth only
+ if P.z <= layDep:
+ PTS.append(FreeCAD.Vector(P.x, P.y, layDep))
+ else:
+ PTS.append(FreeCAD.Vector(P.x, P.y, P.z))
+ # Efor
+
+ if optLinTrans is True:
+ # Remove leading and trailing Hold Points
+ popList = list()
+ for i in range(0, len(PTS)): # identify leading string
+ if PTS[i].z == safe:
+ popList.append(i)
+ else:
+ break
+ popList.sort(reverse=True)
+ for p in popList: # Remove hold points
+ PTS.pop(p)
+ ALL.pop(p)
+ popList = list()
+ for i in range(len(PTS) - 1, -1, -1): # identify trailing string
+ if PTS[i].z == safe:
+ popList.append(i)
+ else:
+ break
+ popList.sort(reverse=True)
+ for p in popList: # Remove hold points
+ PTS.pop(p)
+ ALL.pop(p)
+
+ # Determine max Z height for remaining points on line
+ lMax = obj.FinalDepth.Value
+ if len(ALL) > 0:
+ lMax = ALL[0].z
+ for P in ALL:
+ if P.z > lMax:
+ lMax = P.z
+
+ return (PTS, lMax)
+
+ def _planarMultipassProcess(self, obj, PNTS, lMax):
+ output = list()
+ optimize = obj.OptimizeLinearPaths
+ safe = math.ceil(obj.SafeHeight.Value)
+ lenPNTS = len(PNTS)
+ prcs = True
+ onHold = False
+ onLine = False
+ clrScnLn = lMax + 2.0
+
+ # Initialize first three points
+ nxt = None
+ pnt = PNTS[0]
+ prev = FreeCAD.Vector(-442064564.6, 258539656553.27, 3538553425.847)
+
+ # Add temp end point
+ PNTS.append(FreeCAD.Vector(-4895747464.6, -25855763553.2, 35865763425))
+
+ # Begin processing ocl points list into gcode
+ for i in range(0, lenPNTS):
+ prcs = True
+ nxt = PNTS[i + 1]
+
+ if pnt.z == safe:
+ prcs = False
+ if onHold is False:
+ onHold = True
+ output.append( Path.Command('N (Start hold)', {}) )
+ output.append( Path.Command('G0', {'Z': clrScnLn, 'F': self.vertRapid}) )
+ else:
+ if onHold is True:
+ onHold = False
+ output.append( Path.Command('N (End hold)', {}) )
+ output.append( Path.Command('G0', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizRapid}) )
+
+ # Process point
+ if prcs is True:
+ if optimize is True:
+ # iPOL = prev.isOnLineSegment(nxt, pnt)
+ iPOL = pnt.isOnLineSegment(prev, nxt)
+ if iPOL is True:
+ onLine = True
+ else:
+ onLine = False
+ output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed}))
+ else:
+ output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed}))
+
+ # Rotate point data
+ if onLine is False:
+ prev = pnt
+ pnt = nxt
+ # Efor
+
+ PNTS.pop() # Remove temp end point
+
+ return output
+
+ def _stepTransitionCmds(self, obj, lstPnt, first, minSTH, tolrnc):
+ cmds = list()
+ rtpd = False
+ horizGC = 'G0'
+ hSpeed = self.horizRapid
+ height = obj.SafeHeight.Value
+
+ if obj.CutPattern in ['Line', 'Circular']:
+ if obj.OptimizeStepOverTransitions is True:
+ height = minSTH + 2.0
+ # if obj.LayerMode == 'Multi-pass':
+ # rtpd = minSTH
+ elif obj.CutPattern in ['ZigZag', 'CircularZigZag']:
+ if obj.OptimizeStepOverTransitions is True:
+ zChng = first.z - lstPnt.z
+ # PathLog.debug('first.z: {}'.format(first.z))
+ # PathLog.debug('lstPnt.z: {}'.format(lstPnt.z))
+ # PathLog.debug('zChng: {}'.format(zChng))
+ # PathLog.debug('minSTH: {}'.format(minSTH))
+ if abs(zChng) < tolrnc: # transitions to same Z height
+ PathLog.debug('abs(zChng) < tolrnc')
+ if (minSTH - first.z) > tolrnc:
+ PathLog.debug('(minSTH - first.z) > tolrnc')
+ height = minSTH + 2.0
+ else:
+ PathLog.debug('ELSE (minSTH - first.z) > tolrnc')
+ horizGC = 'G1'
+ height = first.z
+ elif (minSTH + (2.0 * tolrnc)) >= max(first.z, lstPnt.z):
+ height = False # allow end of Zig to cut to beginning of Zag
+
+
+ # Create raise, shift, and optional lower commands
+ if height is not False:
+ cmds.append(Path.Command('G0', {'Z': height, 'F': self.vertRapid}))
+ cmds.append(Path.Command(horizGC, {'X': first.x, 'Y': first.y, 'F': hSpeed}))
+ if rtpd is not False: # ReturnToPreviousDepth
+ cmds.append(Path.Command('G0', {'Z': rtpd, 'F': self.vertRapid}))
+
+ return cmds
+
+ def _breakCmds(self, obj, lstPnt, first, minSTH, tolrnc):
+ cmds = list()
+ rtpd = False
+ horizGC = 'G0'
+ hSpeed = self.horizRapid
+ height = obj.SafeHeight.Value
+
+ if obj.CutPattern in ['Line', 'Circular']:
+ if obj.OptimizeStepOverTransitions is True:
+ height = minSTH + 2.0
+ elif obj.CutPattern in ['ZigZag', 'CircularZigZag']:
+ if obj.OptimizeStepOverTransitions is True:
+ zChng = first.z - lstPnt.z
+ if abs(zChng) < tolrnc: # transitions to same Z height
+ if (minSTH - first.z) > tolrnc:
+ height = minSTH + 2.0
+ else:
+ height = first.z + 2.0 # first.z
+
+ cmds.append(Path.Command('G0', {'Z': height, 'F': self.vertRapid}))
+ cmds.append(Path.Command(horizGC, {'X': first.x, 'Y': first.y, 'F': hSpeed}))
+ if rtpd is not False: # ReturnToPreviousDepth
+ cmds.append(Path.Command('G0', {'Z': rtpd, 'F': self.vertRapid}))
+
+ return cmds
+
+ def _arcsToG2G3(self, LN, numPts, odd, gDIR, tolrnc):
+ cmds = list()
+ strtPnt = LN[0]
+ endPnt = LN[numPts - 1]
+ strtHght = strtPnt.z
+ coPlanar = True
+ isCircle = False
+ gdi = 0
+ if odd is True:
+ gdi = 1
+
+ # Test if pnt set is circle
+ if abs(strtPnt.x - endPnt.x) < tolrnc:
+ if abs(strtPnt.y - endPnt.y) < tolrnc:
+ if abs(strtPnt.z - endPnt.z) < tolrnc:
+ isCircle = True
+ isCircle = False
+
+ if isCircle is True:
+ # convert LN to G2/G3 arc, consolidating GCode
+ # https://wiki.shapeoko.com/index.php/G-Code#G2_-_clockwise_arc
+ # https://www.cnccookbook.com/cnc-g-code-arc-circle-g02-g03/
+ # Dividing circle into two arcs allows for G2/G3 on inclined surfaces
+
+ # ijk = self.tmpCOM - strtPnt # vector from start to center
+ ijk = self.tmpCOM - strtPnt # vector from start to center
+ xyz = self.tmpCOM.add(ijk) # end point
+ cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed}))
+ cmds.append(Path.Command(gDIR[gdi], {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z,
+ 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height
+ 'F': self.horizFeed}))
+ cmds.append(Path.Command('G1', {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z, 'F': self.horizFeed}))
+ ijk = self.tmpCOM - xyz # vector from start to center
+ rst = strtPnt # end point
+ cmds.append(Path.Command(gDIR[gdi], {'X': rst.x, 'Y': rst.y, 'Z': rst.z,
+ 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height
+ 'F': self.horizFeed}))
+ cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed}))
+ else:
+ for pt in LN:
+ if abs(pt.z - strtHght) > tolrnc: # test for horizontal coplanar
+ coPlanar = False
+ break
+ if coPlanar is True:
+ # ijk = self.tmpCOM - strtPnt
+ ijk = self.tmpCOM.sub(strtPnt) # vector from start to center
+ xyz = endPnt
+ cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed}))
+ cmds.append(Path.Command(gDIR[gdi], {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z,
+ 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height
+ 'F': self.horizFeed}))
+ cmds.append(Path.Command('G1', {'X': endPnt.x, 'Y': endPnt.y, 'Z': endPnt.z, 'F': self.horizFeed}))
+
+ return (coPlanar, cmds)
+
+ def _planarApplyDepthOffset(self, SCANDATA, DepthOffset):
+ PathLog.debug('Applying DepthOffset value: {}'.format(DepthOffset))
+ lenScans = len(SCANDATA)
+ for s in range(0, lenScans):
+ SO = SCANDATA[s] # StepOver
+ numParts = len(SO)
+ for prt in range(0, numParts):
+ PRT = SO[prt]
+ if PRT != 'BRK':
+ numPts = len(PRT)
+ for pt in range(0, numPts):
+ SCANDATA[s][prt][pt].z += DepthOffset
+
+ def _planarGetPDC(self, stl, finalDep, SampleInterval, cutter):
+ pdc = ocl.PathDropCutter() # create a pdc [PathDropCutter] object
+ pdc.setSTL(stl) # add stl model
+ pdc.setCutter(cutter) # add cutter
+ pdc.setZ(finalDep) # set minimumZ (final / target depth value)
+ pdc.setSampling(SampleInterval) # set sampling size
+ return pdc
+
+
+ # Main rotational scan functions
+ def _processRotationalOp(self, JOB, obj, mdlIdx, compoundFaces=None):
+ PathLog.debug('_processRotationalOp(self, JOB, obj, mdlIdx, compoundFaces=None)')
+
+ base = JOB.Model.Group[mdlIdx]
+ bb = self.boundBoxes[mdlIdx]
+ stl = self.modelSTLs[mdlIdx]
+
+ # Rotate model to initial index
+ initIdx = obj.CutterTilt + obj.StartIndex
+ if initIdx != 0.0:
+ self.basePlacement = FreeCAD.ActiveDocument.getObject(base.Name).Placement
+ if obj.RotationAxis == 'X':
+ base.Placement = FreeCAD.Placement(FreeCAD.Vector(0.0, 0.0, 0.0), FreeCAD.Rotation(FreeCAD.Vector(1.0, 0.0, 0.0), initIdx))
+ else:
+ base.Placement = FreeCAD.Placement(FreeCAD.Vector(0.0, 0.0, 0.0), FreeCAD.Rotation(FreeCAD.Vector(0.0, 1.0, 0.0), initIdx))
+
+ # Prepare global holdpoint container
+ if self.holdPoint is None:
+ self.holdPoint = FreeCAD.Vector(0.0, 0.0, 0.0)
+ if self.layerEndPnt is None:
+ self.layerEndPnt = FreeCAD.Vector(0.0, 0.0, 0.0)
+
+ # Avoid division by zero in rotational scan calculations
+ if obj.FinalDepth.Value == 0.0:
+ zero = obj.SampleInterval.Value # 0.00001
+ self.FinalDepth = zero
+ # obj.FinalDepth.Value = 0.0
+ else:
+ self.FinalDepth = obj.FinalDepth.Value
+
+ # Determine boundbox radius based upon xzy limits data
+ if math.fabs(bb.ZMin) > math.fabs(bb.ZMax):
+ vlim = bb.ZMin
+ else:
+ vlim = bb.ZMax
+ if obj.RotationAxis == 'X':
+ # Rotation is around X-axis, cutter moves along same axis
+ if math.fabs(bb.YMin) > math.fabs(bb.YMax):
+ hlim = bb.YMin
+ else:
+ hlim = bb.YMax
+ else:
+ # Rotation is around Y-axis, cutter moves along same axis
+ if math.fabs(bb.XMin) > math.fabs(bb.XMax):
+ hlim = bb.XMin
+ else:
+ hlim = bb.XMax
+
+ # Compute max radius of stock, as it rotates, and rotational clearance & safe heights
+ self.bbRadius = math.sqrt(hlim**2 + vlim**2)
+ self.clearHeight = self.bbRadius + JOB.SetupSheet.ClearanceHeightOffset.Value
+ self.safeHeight = self.bbRadius + JOB.SetupSheet.ClearanceHeightOffset.Value
+
+ return self._rotationalDropCutterOp(obj, stl, bb)
+
+ def _rotationalDropCutterOp(self, obj, stl, bb):
+ self.resetTolerance = 0.0000001 # degrees
+ self.layerEndzMax = 0.0
+ commands = []
+ scanLines = []
+ advances = []
+ iSTG = []
+ rSTG = []
+ rings = []
+ lCnt = 0
+ rNum = 0
+ bbRad = self.bbRadius
+
+ def invertAdvances(advances):
+ idxs = [1.1]
+ for adv in advances:
+ idxs.append(-1 * adv)
+ idxs.pop(0)
+ return idxs
+
+ def linesToPointRings(scanLines):
+ rngs = []
+ numPnts = len(scanLines[0]) # Number of points per line along axis, at obj.SampleInterval.Value spacing
+ for line in scanLines: # extract circular set(ring) of points from scan lines
+ if len(line) != numPnts:
+ PathLog.debug('Error: line lengths not equal')
+ return rngs
+
+ for num in range(0, numPnts):
+ rngs.append([1.1]) # Initiate new ring
+ for line in scanLines: # extract circular set(ring) of points from scan lines
+ rngs[num].append(line[num])
+ rngs[num].pop(0)
+ return rngs
+
+ def indexAdvances(arc, stepDeg):
+ indexes = [0.0]
+ numSteps = int(math.floor(arc / stepDeg))
+ for ns in range(0, numSteps):
+ indexes.append(stepDeg)
+
+ travel = sum(indexes)
+ if arc == 360.0:
+ indexes.insert(0, 0.0)
+ else:
+ indexes.append(arc - travel)
+
+ return indexes
+
+ # Compute number and size of stepdowns, and final depth
+ if obj.LayerMode == 'Single-pass':
+ depthparams = [self.FinalDepth]
+ else:
+ dep_par = PathUtils.depth_params(self.clearHeight, self.safeHeight, self.bbRadius, obj.StepDown.Value, 0.0, self.FinalDepth)
+ depthparams = [i for i in dep_par]
+ prevDepth = depthparams[0]
+ lenDP = len(depthparams)
+
+ # Set drop cutter extra offset
+ cdeoX = obj.DropCutterExtraOffset.x
+ cdeoY = obj.DropCutterExtraOffset.y
+
+ # Set updated bound box values and redefine the new min/mas XY area of the operation based on greatest point radius of model
+ bb.ZMin = -1 * bbRad
+ bb.ZMax = bbRad
+ if obj.RotationAxis == 'X':
+ bb.YMin = -1 * bbRad
+ bb.YMax = bbRad
+ ymin = 0.0
+ ymax = 0.0
+ xmin = bb.XMin - cdeoX
+ xmax = bb.XMax + cdeoX
+ else:
+ bb.XMin = -1 * bbRad
+ bb.XMax = bbRad
+ ymin = bb.YMin - cdeoY
+ ymax = bb.YMax + cdeoY
+ xmin = 0.0
+ xmax = 0.0
+
+ # Calculate arc
+ begIdx = obj.StartIndex
+ endIdx = obj.StopIndex
+ if endIdx < begIdx:
+ begIdx -= 360.0
+ arc = endIdx - begIdx
+
+ # Begin gcode operation with raising cutter to safe height
+ commands.append(Path.Command('G0', {'Z': self.safeHeight, 'F': self.vertRapid}))
+
+ # Complete rotational scans at layer and translate into gcode
+ for layDep in depthparams:
+ t_before = time.time()
+
+ # Compute circumference and step angles for current layer
+ layCircum = 2 * math.pi * layDep
+ if lenDP == 1:
+ layCircum = 2 * math.pi * bbRad
+
+ # Set axial feed rates
+ self.axialFeed = 360 / layCircum * self.horizFeed
+ self.axialRapid = 360 / layCircum * self.horizRapid
+
+ # Determine step angle.
+ if obj.RotationAxis == obj.DropCutterDir: # Same == indexed
+ stepDeg = (self.cutOut / layCircum) * 360.0
+ else:
+ stepDeg = (obj.SampleInterval.Value / layCircum) * 360.0
+
+ # Limit step angle and determine rotational index angles [indexes].
+ if stepDeg > 120.0:
+ stepDeg = 120.0
+ advances = indexAdvances(arc, stepDeg) # Reset for each step down layer
+
+ # Perform rotational indexed scans to layer depth
+ if obj.RotationAxis == obj.DropCutterDir: # Same == indexed OR parallel
+ sample = obj.SampleInterval.Value
+ else:
+ sample = self.cutOut
+ scanLines = self._indexedDropCutScan(obj, stl, advances, xmin, ymin, xmax, ymax, layDep, sample)
+
+ # Complete rotation if necessary
+ if arc == 360.0:
+ advances.append(360.0 - sum(advances))
+ advances.pop(0)
+ zero = scanLines.pop(0)
+ scanLines.append(zero)
+
+ # Translate OCL scans into gcode
+ if obj.RotationAxis == obj.DropCutterDir: # Same == indexed (cutter runs parallel to axis)
+
+ # Translate scan to gcode
+ sumAdv = begIdx
+ for sl in range(0, len(scanLines)):
+ sumAdv += advances[sl]
+ # Translate scan to gcode
+ iSTG = self._indexedScanToGcode(obj, sl, scanLines[sl], sumAdv, prevDepth, layDep, lenDP)
+ commands.extend(iSTG)
+
+ # Raise cutter to safe height after each index cut
+ commands.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid}))
+ # Eol
+ else:
+ if self.CutClimb is False:
+ advances = invertAdvances(advances)
+ advances.reverse()
+ scanLines.reverse()
+
+ # Begin gcode operation with raising cutter to safe height
+ commands.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid}))
+
+ # Convert rotational scans into gcode
+ rings = linesToPointRings(scanLines)
+ rNum = 0
+ for rng in rings:
+ rSTG = self._rotationalScanToGcode(obj, rng, rNum, prevDepth, layDep, advances)
+ commands.extend(rSTG)
+ if arc != 360.0:
+ clrZ = self.layerEndzMax + self.SafeHeightOffset
+ commands.append(Path.Command('G0', {'Z': clrZ, 'F': self.vertRapid}))
+ rNum += 1
+ # Eol
+
+ prevDepth = layDep
+ lCnt += 1 # increment layer count
+ PathLog.debug("--Layer " + str(lCnt) + ": " + str(len(advances)) + " OCL scans and gcode in " + str(time.time() - t_before) + " s")
+ # Eol
+
+ return commands
+
+ def _indexedDropCutScan(self, obj, stl, advances, xmin, ymin, xmax, ymax, layDep, sample):
+ cutterOfst = 0.0
+ iCnt = 0
+ Lines = []
+ result = None
+
+ pdc = ocl.PathDropCutter() # create a pdc
+ pdc.setCutter(self.cutter)
+ pdc.setZ(layDep) # set minimumZ (final / ta9rget depth value)
+ pdc.setSampling(sample)
+
+ # if self.useTiltCutter == True:
+ if obj.CutterTilt != 0.0:
+ cutterOfst = layDep * math.sin(math.radians(obj.CutterTilt))
+ PathLog.debug("CutterTilt: cutterOfst is " + str(cutterOfst))
+
+ sumAdv = 0.0
+ for adv in advances:
+ sumAdv += adv
+ if adv > 0.0:
+ # Rotate STL object using OCL method
+ radsRot = math.radians(adv)
+ if obj.RotationAxis == 'X':
+ stl.rotate(radsRot, 0.0, 0.0)
+ else:
+ stl.rotate(0.0, radsRot, 0.0)
+
+ # Set STL after rotation is made
+ pdc.setSTL(stl)
+
+ # add Line objects to the path in this loop
+ if obj.RotationAxis == 'X':
+ p1 = ocl.Point(xmin, cutterOfst, 0.0) # start-point of line
+ p2 = ocl.Point(xmax, cutterOfst, 0.0) # end-point of line
+ else:
+ p1 = ocl.Point(cutterOfst, ymin, 0.0) # start-point of line
+ p2 = ocl.Point(cutterOfst, ymax, 0.0) # end-point of line
+
+ # Create line object
+ if obj.RotationAxis == obj.DropCutterDir: # parallel cut
+ if obj.CutPattern == 'ZigZag':
+ if (iCnt % 2 == 0.0): # even
+ lo = ocl.Line(p1, p2)
+ else: # odd
+ lo = ocl.Line(p2, p1)
+ elif obj.CutPattern == 'Line':
+ if self.CutClimb is True:
+ lo = ocl.Line(p2, p1)
+ else:
+ lo = ocl.Line(p1, p2)
+ else:
+ lo = ocl.Line(p1, p2) # line-object
+
+ path = ocl.Path() # create an empty path object
+ path.append(lo) # add the line to the path
+ pdc.setPath(path) # set path
+ pdc.run() # run drop-cutter on the path
+ result = pdc.getCLPoints() # request the list of points
+
+ # Convert list of OCL objects to list of Vectors for faster access and Apply depth offset
+ if obj.DepthOffset.Value != 0.0:
+ Lines.append([FreeCAD.Vector(p.x, p.y, p.z + obj.DepthOffset.Value) for p in result])
+ else:
+ Lines.append([FreeCAD.Vector(p.x, p.y, p.z) for p in result])
+
+ iCnt += 1
+ # End loop
+
+ # Rotate STL object back to original position using OCL method
+ reset = -1 * math.radians(sumAdv - self.resetTolerance)
+ if obj.RotationAxis == 'X':
+ stl.rotate(reset, 0.0, 0.0)
+ else:
+ stl.rotate(0.0, reset, 0.0)
+ self.resetTolerance = 0.0
+
+ return Lines
+
+ def _indexedScanToGcode(self, obj, li, CLP, idxAng, prvDep, layerDepth, numDeps):
+ # generate the path commands
+ output = []
+ optimize = obj.OptimizeLinearPaths
+ holdCount = 0
+ holdStart = False
+ holdStop = False
+ zMax = prvDep
+ lenCLP = len(CLP)
+ lastCLP = lenCLP - 1
+ prev = FreeCAD.Vector(0.0, 0.0, 0.0)
+ nxt = FreeCAD.Vector(0.0, 0.0, 0.0)
+
+ # Create first point
+ pnt = CLP[0]
+
+ # Rotate to correct index location
+ if obj.RotationAxis == 'X':
+ output.append(Path.Command('G0', {'A': idxAng, 'F': self.axialFeed}))
+ else:
+ output.append(Path.Command('G0', {'B': idxAng, 'F': self.axialFeed}))
+
+ if li > 0:
+ if pnt.z > self.layerEndPnt.z:
+ clrZ = pnt.z + 2.0
+ output.append(Path.Command('G1', {'Z': clrZ, 'F': self.vertRapid}))
+ else:
+ output.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid}))
+
+ output.append(Path.Command('G0', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizRapid}))
+ output.append(Path.Command('G1', {'Z': pnt.z, 'F': self.vertFeed}))
+
+ for i in range(0, lenCLP):
+ if i < lastCLP:
+ nxt = CLP[i + 1]
+ else:
+ optimize = False
+
+ # Update zMax values
+ if pnt.z > zMax:
+ zMax = pnt.z
+
+ if obj.LayerMode == 'Multi-pass':
+ # if z travels above previous layer, start/continue hold high cycle
+ if pnt.z > prvDep and optimize is True:
+ if self.onHold is False:
+ holdStart = True
+ self.onHold = True
+
+ if self.onHold is True:
+ if holdStart is True:
+ # go to current coordinate
+ output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed}))
+ # Save holdStart coordinate and prvDep values
+ self.holdPoint = pnt
+ holdCount += 1 # Increment hold count
+ holdStart = False # cancel holdStart
+
+ # hold cutter high until Z value drops below prvDep
+ if pnt.z <= prvDep:
+ holdStop = True
+
+ if holdStop is True:
+ # Send hold and current points to
+ zMax += 2.0
+ for cmd in self.holdStopCmds(obj, zMax, prvDep, pnt, "Hold Stop: in-line"):
+ output.append(cmd)
+ # reset necessary hold related settings
+ zMax = prvDep
+ holdStop = False
+ self.onHold = False
+ self.holdPoint = FreeCAD.Vector(0.0, 0.0, 0.0)
+
+ if self.onHold is False:
+ if not optimize or not pnt.isOnLineSegment(prev, nxt):
+ output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed}))
+
+ # Rotate point data
+ prev = pnt
+ pnt = nxt
+ output.append(Path.Command('N (End index angle ' + str(round(idxAng, 4)) + ')', {}))
+
+ # Save layer end point for use in transitioning to next layer
+ self.layerEndPnt = pnt
+
+ return output
+
+ def _rotationalScanToGcode(self, obj, RNG, rN, prvDep, layDep, advances):
+ '''_rotationalScanToGcode(obj, RNG, rN, prvDep, layDep, advances) ...
+ Convert rotational scan data to gcode path commands.'''
+ output = []
+ nxtAng = 0
+ zMax = 0.0
+ nxt = FreeCAD.Vector(0.0, 0.0, 0.0)
+
+ begIdx = obj.StartIndex
+ endIdx = obj.StopIndex
+ if endIdx < begIdx:
+ begIdx -= 360.0
+
+ # Rotate to correct index location
+ axisOfRot = 'A'
+ if obj.RotationAxis == 'Y':
+ axisOfRot = 'B'
+
+ # Create first point
+ ang = 0.0 + obj.CutterTilt
+ pnt = RNG[0]
+
+ # Adjust feed rate based on radius/circumference of cutter.
+ # Original feed rate based on travel at circumference.
+ if rN > 0:
+ if pnt.z >= self.layerEndzMax:
+ clrZ = pnt.z + 5.0
+ output.append(Path.Command('G1', {'Z': clrZ, 'F': self.vertRapid}))
+ else:
+ output.append(Path.Command('G1', {'Z': self.clearHeight, 'F': self.vertRapid}))
+
+ output.append(Path.Command('G0', {axisOfRot: ang, 'F': self.axialFeed}))
+ output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'F': self.axialFeed}))
+ output.append(Path.Command('G1', {'Z': pnt.z, 'F': self.axialFeed}))
+
+ lenRNG = len(RNG)
+ lastIdx = lenRNG - 1
+ for i in range(0, lenRNG):
+ if i < lastIdx:
+ nxtAng = ang + advances[i + 1]
+ nxt = RNG[i + 1]
+
+ if pnt.z > zMax:
+ zMax = pnt.z
+
+ output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, axisOfRot: ang, 'F': self.axialFeed}))
+ pnt = nxt
+ ang = nxtAng
+
+ # Save layer end point for use in transitioning to next layer
+ self.layerEndPnt = RNG[0]
+ self.layerEndzMax = zMax
+
+ return output
+
+ def holdStopCmds(self, obj, zMax, pd, p2, txt):
+ '''holdStopCmds(obj, zMax, pd, p2, txt) ... Gcode commands to be executed at beginning of hold.'''
+ cmds = []
+ msg = 'N (' + txt + ')'
+ cmds.append(Path.Command(msg, {})) # Raise cutter rapid to zMax in line of travel
+ cmds.append(Path.Command('G0', {'Z': zMax, 'F': self.vertRapid})) # Raise cutter rapid to zMax in line of travel
+ cmds.append(Path.Command('G0', {'X': p2.x, 'Y': p2.y, 'F': self.horizRapid})) # horizontal rapid to current XY coordinate
+ if zMax != pd:
+ cmds.append(Path.Command('G0', {'Z': pd, 'F': self.vertRapid})) # drop cutter down rapidly to prevDepth depth
+ cmds.append(Path.Command('G0', {'Z': p2.z, 'F': self.vertFeed})) # drop cutter down to current Z depth, returning to normal cut path and speed
+ return cmds
+
+ # Additional support methods
+ def resetOpVariables(self, all=True):
+ '''resetOpVariables() ... Reset class variables used for instance of operation.'''
+ self.holdPoint = None
+ self.layerEndPnt = None
+ self.onHold = False
+ self.SafeHeightOffset = 2.0
+ self.ClearHeightOffset = 4.0
+ self.layerEndzMax = 0.0
+ self.resetTolerance = 0.0
+ self.holdPntCnt = 0
+ self.bbRadius = 0.0
+ self.axialFeed = 0.0
+ self.axialRapid = 0.0
+ self.FinalDepth = 0.0
+ self.clearHeight = 0.0
+ self.safeHeight = 0.0
+ self.faceZMax = -999999999999.0
+ if all is True:
+ self.cutter = None
+ self.stl = None
+ self.fullSTL = None
+ self.cutOut = 0.0
+ self.radius = 0.0
+ self.useTiltCutter = False
+ return True
+
+ def deleteOpVariables(self, all=True):
+ '''deleteOpVariables() ... Reset class variables used for instance of operation.'''
+ del self.holdPoint
+ del self.layerEndPnt
+ del self.onHold
+ del self.SafeHeightOffset
+ del self.ClearHeightOffset
+ del self.layerEndzMax
+ del self.resetTolerance
+ del self.holdPntCnt
+ del self.bbRadius
+ del self.axialFeed
+ del self.axialRapid
+ del self.FinalDepth
+ del self.clearHeight
+ del self.safeHeight
+ del self.faceZMax
+ if all is True:
+ del self.cutter
+ del self.stl
+ del self.fullSTL
+ del self.cutOut
+ del self.radius
+ del self.useTiltCutter
+ return True
+
+ def setOclCutter(self, obj, safe=False):
+ ''' setOclCutter(obj) ... Translation function to convert FreeCAD tool definition to OCL formatted tool. '''
+ # Set cutter details
+ # https://www.freecadweb.org/api/dd/dfe/classPath_1_1Tool.html#details
+ diam_1 = float(obj.ToolController.Tool.Diameter)
+ lenOfst = obj.ToolController.Tool.LengthOffset if hasattr(obj.ToolController.Tool, 'LengthOffset') else 0
+ FR = obj.ToolController.Tool.FlatRadius if hasattr(obj.ToolController.Tool, 'FlatRadius') else 0
+ CEH = obj.ToolController.Tool.CuttingEdgeHeight if hasattr(obj.ToolController.Tool, 'CuttingEdgeHeight') else 0
+ CEA = obj.ToolController.Tool.CuttingEdgeAngle if hasattr(obj.ToolController.Tool, 'CuttingEdgeAngle') else 0
+
+ # Make safeCutter with 2 mm buffer around physical cutter
+ if safe is True:
+ diam_1 += 4.0
+ if FR != 0.0:
+ FR += 2.0
+
+ PathLog.debug('ToolType: {}'.format(obj.ToolController.Tool.ToolType))
+ if obj.ToolController.Tool.ToolType == 'EndMill':
+ # Standard End Mill
+ return ocl.CylCutter(diam_1, (CEH + lenOfst))
+
+ elif obj.ToolController.Tool.ToolType == 'BallEndMill' and FR == 0.0:
+ # Standard Ball End Mill
+ # OCL -> BallCutter::BallCutter(diameter, length)
+ self.useTiltCutter = True
+ return ocl.BallCutter(diam_1, (diam_1 / 2 + lenOfst))
+
+ elif obj.ToolController.Tool.ToolType == 'BallEndMill' and FR > 0.0:
+ # Bull Nose or Corner Radius cutter
+ # Reference: https://www.fine-tools.com/halbstabfraeser.html
+ # OCL -> BallCutter::BallCutter(diameter, length)
+ return ocl.BullCutter(diam_1, FR, (CEH + lenOfst))
+
+ elif obj.ToolController.Tool.ToolType == 'Engraver' and FR > 0.0:
+ # Bull Nose or Corner Radius cutter
+ # Reference: https://www.fine-tools.com/halbstabfraeser.html
+ # OCL -> ConeCutter::ConeCutter(diameter, angle, lengthOffset)
+ return ocl.ConeCutter(diam_1, (CEA / 2), lenOfst)
+
+ elif obj.ToolController.Tool.ToolType == 'ChamferMill':
+ # Bull Nose or Corner Radius cutter
+ # Reference: https://www.fine-tools.com/halbstabfraeser.html
+ # OCL -> ConeCutter::ConeCutter(diameter, angle, lengthOffset)
+ return ocl.ConeCutter(diam_1, (CEA / 2), lenOfst)
+ else:
+ # Default to standard end mill
+ PathLog.warning("Defaulting cutter to standard end mill.")
+ return ocl.CylCutter(diam_1, (CEH + lenOfst))
+
+ def _getMinSafeTravelHeight(self, pdc, p1, p2, minDep=None):
+ A = (p1.x, p1.y)
+ B = (p2.x, p2.y)
+ LINE = self._planarDropCutScan(pdc, A, B)
+ zMax = max([obj.z for obj in LINE])
+ if minDep is not None:
+ if zMax < minDep:
+ zMax = minDep
+ return zMax
+
+
+def SetupProperties():
+ ''' SetupProperties() ... Return list of properties required for operation.'''
+ setup = ['AvoidLastX_Faces', 'AvoidLastX_InternalFeatures', 'BoundBox']
+ setup.extend(['BoundaryAdjustment', 'PatternCenterAt', 'PatternCenterCustom'])
+ setup.extend(['CircularUseG2G3', 'InternalFeaturesCut', 'InternalFeaturesAdjustment'])
+ setup.extend(['CutMode', 'CutPattern', 'CutPatternAngle', 'CutPatternReversed'])
+ setup.extend(['CutterTilt', 'DepthOffset', 'DropCutterDir', 'GapSizes', 'GapThreshold'])
+ setup.extend(['HandleMultipleFeatures', 'LayerMode', 'OptimizeStepOverTransitions'])
+ setup.extend(['ProfileEdges', 'BoundaryEnforcement', 'RotationAxis', 'SampleInterval'])
+ setup.extend(['ScanType', 'StartIndex', 'StartPoint', 'StepOver', 'StopIndex'])
+ setup.extend(['UseStartPoint', 'AngularDeflection', 'LinearDeflection', 'ShowTempObjects'])
+ return setup
+
+
+def Create(name, obj=None):
+ '''Create(name) ... Creates and returns a Surface operation.'''
+ if obj is None:
+ obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
+ obj.Proxy = ObjectSurface(obj, name)
+ return obj
diff --git a/src/Mod/Path/PathScripts/PathSurfaceGui.py b/src/Mod/Path/PathScripts/PathSurfaceGui.py
index 41f11f6007..7ff1342360 100644
--- a/src/Mod/Path/PathScripts/PathSurfaceGui.py
+++ b/src/Mod/Path/PathScripts/PathSurfaceGui.py
@@ -41,7 +41,7 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
def initPage(self, obj):
self.setTitle("3D Surface")
- self.updateVisibility()
+ # self.updateVisibility()
def getForm(self):
'''getForm() ... returns UI'''
@@ -118,6 +118,8 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
else:
self.form.optimizeStepOverTransitions.setCheckState(QtCore.Qt.Unchecked)
+ self.updateVisibility()
+
def getSignalsForUpdate(self, obj):
'''getSignalsForUpdate(obj) ... return list of signals for updating obj'''
signals = []
@@ -140,16 +142,26 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
return signals
def updateVisibility(self):
- if self.form.scanType.currentText() == "Planar":
- self.form.cutPattern.setEnabled(True)
- self.form.boundBoxExtraOffsetX.setEnabled(False)
- self.form.boundBoxExtraOffsetY.setEnabled(False)
- self.form.dropCutterDirSelect.setEnabled(False)
- else:
- self.form.cutPattern.setEnabled(False)
- self.form.boundBoxExtraOffsetX.setEnabled(True)
- self.form.boundBoxExtraOffsetY.setEnabled(True)
- self.form.dropCutterDirSelect.setEnabled(True)
+ if self.form.scanType.currentText() == 'Planar':
+ self.form.cutPattern.show()
+ self.form.cutPattern_label.show()
+ self.form.optimizeStepOverTransitions.show()
+
+ self.form.boundBoxExtraOffsetX.hide()
+ self.form.boundBoxExtraOffsetY.hide()
+ self.form.boundBoxExtraOffset_label.hide()
+ self.form.dropCutterDirSelect.hide()
+ self.form.dropCutterDirSelect_label.hide()
+ elif self.form.scanType.currentText() == 'Rotational':
+ self.form.cutPattern.hide()
+ self.form.cutPattern_label.hide()
+ self.form.optimizeStepOverTransitions.hide()
+
+ self.form.boundBoxExtraOffsetX.show()
+ self.form.boundBoxExtraOffsetY.show()
+ self.form.boundBoxExtraOffset_label.show()
+ self.form.dropCutterDirSelect.show()
+ self.form.dropCutterDirSelect_label.show()
def registerSignalHandlers(self, obj):
self.form.scanType.currentIndexChanged.connect(self.updateVisibility)
diff --git a/src/Mod/Path/PathScripts/PathSurfaceSupport.py b/src/Mod/Path/PathScripts/PathSurfaceSupport.py
new file mode 100644
index 0000000000..e991b28163
--- /dev/null
+++ b/src/Mod/Path/PathScripts/PathSurfaceSupport.py
@@ -0,0 +1,1855 @@
+# -*- coding: utf-8 -*-
+
+# ***************************************************************************
+# * *
+# * Copyright (c) 2020 russ4262 *
+# * *
+# * 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 __future__ import print_function
+
+__title__ = "Path Surface Support Module"
+__author__ = "russ4262 (Russell Johnson)"
+__url__ = "http://www.freecadweb.org"
+__doc__ = "Support functions and classes for 3D Surface and Waterline operations."
+# __name__ = "PathSurfaceSupport"
+__contributors__ = ""
+
+import FreeCAD
+from PySide import QtCore
+import Path
+import PathScripts.PathLog as PathLog
+import PathScripts.PathUtils as PathUtils
+import math
+import Part
+
+
+PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
+# PathLog.trackModule(PathLog.thisModule())
+
+
+# Qt translation handling
+def translate(context, text, disambig=None):
+ return QtCore.QCoreApplication.translate(context, text, disambig)
+
+
+class PathGeometryGenerator:
+ '''Creates a path geometry shape from an assigned pattern for conversion to tool paths.
+ PathGeometryGenerator(obj, shape, pattern)
+ `obj` is the operation object, `shape` is the horizontal planar shape object,
+ and `pattern` is the name of the geometric pattern to apply.
+ Frist, call the getCenterOfPattern() method for the CenterOfMass for patterns allowing a custom center.
+ Next, call the generatePathGeometry() method to request the path geometry shape.'''
+
+ # Register valid patterns here by name
+ # Create a corresponding processing method below. Precede the name with an underscore(_)
+ patterns = ('Circular', 'CircularZigZag', 'Line', 'Offset', 'Spiral', 'ZigZag')
+
+ def __init__(self, obj, shape, pattern):
+ '''__init__(obj, shape, pattern)... Instantiate PathGeometryGenerator class.
+ Required arguments are the operation object, horizontal planar shape, and pattern name.'''
+ self.debugObjectsGroup = False
+ self.pattern = 'None'
+ self.shape = None
+ self.pathGeometry = None
+ self.rawGeoList = None
+ self.centerOfMass = None
+ self.centerofPattern = None
+ self.deltaX = None
+ self.deltaY = None
+ self.deltaC = None
+ self.halfDiag = None
+ self.halfPasses = None
+ self.obj = obj
+ self.toolDiam = float(obj.ToolController.Tool.Diameter)
+ self.cutOut = self.toolDiam * (float(obj.StepOver) / 100.0)
+ self.wpc = Part.makeCircle(2.0) # make circle for workplane
+
+ # validate requested pattern
+ if pattern in self.patterns:
+ if hasattr(self, '_' + pattern):
+ self.pattern = pattern
+
+ if shape.BoundBox.ZMin != 0.0:
+ shape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - shape.BoundBox.ZMin))
+ if shape.BoundBox.ZLength == 0.0:
+ self.shape = shape
+ else:
+ PathLog.warning('Shape appears to not be horizontal planar. ZMax is {}.'.format(shape.BoundBox.ZMax))
+
+ self._prepareConstants()
+
+ def _prepareConstants(self):
+ # Apply drop cutter extra offset and set the max and min XY area of the operation
+ xmin = self.shape.BoundBox.XMin
+ xmax = self.shape.BoundBox.XMax
+ ymin = self.shape.BoundBox.YMin
+ ymax = self.shape.BoundBox.YMax
+
+ # Compute weighted center of mass of all faces combined
+ if self.pattern in ['Circular', 'CircularZigZag', 'Spiral']:
+ if self.obj.PatternCenterAt == 'CenterOfMass':
+ fCnt = 0
+ totArea = 0.0
+ zeroCOM = FreeCAD.Vector(0.0, 0.0, 0.0)
+ for F in self.shape.Faces:
+ comF = F.CenterOfMass
+ areaF = F.Area
+ totArea += areaF
+ fCnt += 1
+ zeroCOM = zeroCOM.add(FreeCAD.Vector(comF.x, comF.y, 0.0).multiply(areaF))
+ if fCnt == 0:
+ PathLog.error(translate(self.module, 'Cannot calculate the Center Of Mass. Using Center of Boundbox instead.'))
+ bbC = self.shape.BoundBox.Center
+ zeroCOM = FreeCAD.Vector(bbC.x, bbC.y, 0.0)
+ else:
+ avgArea = totArea / fCnt
+ zeroCOM.multiply(1 / fCnt)
+ zeroCOM.multiply(1 / avgArea)
+ self.centerOfMass = FreeCAD.Vector(zeroCOM.x, zeroCOM.y, 0.0)
+ self.centerOfPattern = self._getPatternCenter()
+ else:
+ bbC = self.shape.BoundBox.Center
+ self.centerOfPattern = FreeCAD.Vector(bbC.x, bbC.y, 0.0)
+
+ # get X, Y, Z spans; Compute center of rotation
+ self.deltaX = self.shape.BoundBox.XLength
+ self.deltaY = self.shape.BoundBox.YLength
+ self.deltaC = self.shape.BoundBox.DiagonalLength # math.sqrt(self.deltaX**2 + self.deltaY**2)
+ lineLen = self.deltaC + (2.0 * self.toolDiam) # Line length to span boundbox diag with 2x cutter diameter extra on each end
+ self.halfDiag = math.ceil(lineLen / 2.0)
+ cutPasses = math.ceil(lineLen / self.cutOut) + 1 # Number of lines(passes) required to cover boundbox diagonal
+ self.halfPasses = math.ceil(cutPasses / 2.0)
+
+ # Public methods
+ def setDebugObjectsGroup(self, tmpGrpObject):
+ '''setDebugObjectsGroup(tmpGrpObject)...
+ Pass the temporary object group to show temporary construction objects'''
+ self.debugObjectsGroup = tmpGrpObject
+
+ def getCenterOfPattern(self):
+ '''getCenterOfPattern()...
+ Returns the Center Of Mass for the current class instance.'''
+ return self.centerOfPattern
+
+ def generatePathGeometry(self):
+ '''generatePathGeometry()...
+ Call this function to obtain the path geometry shape, generated by this class.'''
+ if self.pattern == 'None':
+ PathLog.warning('PGG: No pattern set.')
+ return False
+
+ if self.shape is None:
+ PathLog.warning('PGG: No shape set.')
+ return False
+
+ cmd = 'self._' + self.pattern + '()'
+ exec(cmd)
+
+ if self.obj.CutPatternReversed is True:
+ self.rawGeoList.reverse()
+
+ # Create compound object to bind all lines in Lineset
+ geomShape = Part.makeCompound(self.rawGeoList)
+
+ # Position and rotate the Line and ZigZag geometry
+ if self.pattern in ['Line', 'ZigZag']:
+ if self.obj.CutPatternAngle != 0.0:
+ geomShape.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), self.obj.CutPatternAngle)
+ bbC = self.shape.BoundBox.Center
+ geomShape.Placement.Base = FreeCAD.Vector(bbC.x, bbC.y, 0.0 - geomShape.BoundBox.ZMin)
+
+ if self.debugObjectsGroup:
+ F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpGeometrySet')
+ F.Shape = geomShape
+ F.purgeTouched()
+ self.debugObjectsGroup.addObject(F)
+
+ if self.pattern == 'Offset':
+ return geomShape
+
+ # Identify intersection of cross-section face and lineset
+ cmnShape = self.shape.common(geomShape)
+
+ if self.debugObjectsGroup:
+ F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpPathGeometry')
+ F.Shape = cmnShape
+ F.purgeTouched()
+ self.debugObjectsGroup.addObject(F)
+
+ return cmnShape
+
+ # Cut pattern methods
+ def _Circular(self):
+ GeoSet = list()
+ radialPasses = self._getRadialPasses()
+ minRad = self.toolDiam * 0.45
+ siX3 = 3 * self.obj.SampleInterval.Value
+ minRadSI = (siX3 / 2.0) / math.pi
+
+ if minRad < minRadSI:
+ minRad = minRadSI
+
+ PathLog.debug(' -centerOfPattern: {}'.format(self.centerOfPattern))
+ # Make small center circle to start pattern
+ if self.obj.StepOver > 50:
+ circle = Part.makeCircle(minRad, self.centerOfPattern)
+ GeoSet.append(circle)
+
+ for lc in range(1, radialPasses + 1):
+ rad = (lc * self.cutOut)
+ if rad >= minRad:
+ circle = Part.makeCircle(rad, self.centerOfPattern)
+ GeoSet.append(circle)
+ # Efor
+ self.rawGeoList = GeoSet
+
+ def _CircularZigZag(self):
+ self._Circular() # Use _Circular generator
+
+ def _Line(self):
+ GeoSet = list()
+ centRot = FreeCAD.Vector(0.0, 0.0, 0.0) # Bottom left corner of face/selection/model
+ cAng = math.atan(self.deltaX / self.deltaY) # BoundaryBox angle
+
+ # Determine end points and create top lines
+ x1 = centRot.x - self.halfDiag
+ x2 = centRot.x + self.halfDiag
+ diag = None
+ if self.obj.CutPatternAngle == 0 or self.obj.CutPatternAngle == 180:
+ diag = self.deltaY
+ elif self.obj.CutPatternAngle == 90 or self.obj.CutPatternAngle == 270:
+ diag = self.deltaX
+ else:
+ perpDist = math.cos(cAng - math.radians(self.obj.CutPatternAngle)) * self.deltaC
+ diag = perpDist
+ y1 = centRot.y + diag
+ # y2 = y1
+
+ # Create end points for set of lines to intersect with cross-section face
+ pntTuples = list()
+ for lc in range((-1 * (self.halfPasses - 1)), self.halfPasses + 1):
+ x1 = centRot.x - self.halfDiag
+ x2 = centRot.x + self.halfDiag
+ y1 = centRot.y + (lc * self.cutOut)
+ # y2 = y1
+ p1 = FreeCAD.Vector(x1, y1, 0.0)
+ p2 = FreeCAD.Vector(x2, y1, 0.0)
+ pntTuples.append((p1, p2))
+
+ # Convert end points to lines
+ for (p1, p2) in pntTuples:
+ line = Part.makeLine(p1, p2)
+ GeoSet.append(line)
+
+ self.rawGeoList = GeoSet
+
+ def _Offset(self):
+ self.rawGeoList = self._extractOffsetFaces()
+
+ def _Spiral(self):
+ GeoSet = list()
+ SEGS = list()
+ draw = True
+ loopRadians = 0.0 # Used to keep track of complete loops/cycles
+ sumRadians = 0.0
+ loopCnt = 0
+ segCnt = 0
+ twoPi = 2.0 * math.pi
+ maxDist = math.ceil(self.cutOut * self._getRadialPasses()) # self.halfDiag
+ move = self.centerOfPattern # Use to translate the center of the spiral
+ lastPoint = FreeCAD.Vector(0.0, 0.0, 0.0)
+
+ # Set tool properties and calculate cutout
+ cutOut = self.cutOut / twoPi
+ segLen = self.obj.SampleInterval.Value # CutterDiameter / 10.0 # SampleInterval.Value
+ stepAng = segLen / ((loopCnt + 1) * self.cutOut) # math.pi / 18.0 # 10 degrees
+ stopRadians = maxDist / cutOut
+
+ if self.obj.CutPatternReversed:
+ if self.obj.CutMode == 'Conventional':
+ getPoint = self._makeOppSpiralPnt
+ else:
+ getPoint = self._makeRegSpiralPnt
+
+ while draw:
+ radAng = sumRadians + stepAng
+ p1 = lastPoint
+ p2 = getPoint(move, cutOut, radAng) # cutOut is 'b' in the equation r = b * radAng
+ sumRadians += stepAng # Increment sumRadians
+ loopRadians += stepAng # Increment loopRadians
+ if loopRadians > twoPi:
+ loopCnt += 1
+ loopRadians -= twoPi
+ stepAng = segLen / ((loopCnt + 1) * self.cutOut) # adjust stepAng with each loop/cycle
+ segCnt += 1
+ lastPoint = p2
+ if sumRadians > stopRadians:
+ draw = False
+ # Create line and show in Object tree
+ lineSeg = Part.makeLine(p2, p1)
+ SEGS.append(lineSeg)
+ # Ewhile
+ SEGS.reverse()
+ else:
+ if self.obj.CutMode == 'Climb':
+ getPoint = self._makeOppSpiralPnt
+ else:
+ getPoint = self._makeRegSpiralPnt
+
+ while draw:
+ radAng = sumRadians + stepAng
+ p1 = lastPoint
+ p2 = getPoint(move, cutOut, radAng) # cutOut is 'b' in the equation r = b * radAng
+ sumRadians += stepAng # Increment sumRadians
+ loopRadians += stepAng # Increment loopRadians
+ if loopRadians > twoPi:
+ loopCnt += 1
+ loopRadians -= twoPi
+ stepAng = segLen / ((loopCnt + 1) * self.cutOut) # adjust stepAng with each loop/cycle
+ segCnt += 1
+ lastPoint = p2
+ if sumRadians > stopRadians:
+ draw = False
+ # Create line and show in Object tree
+ lineSeg = Part.makeLine(p1, p2)
+ SEGS.append(lineSeg)
+ # Ewhile
+ # Eif
+ spiral = Part.Wire([ls.Edges[0] for ls in SEGS])
+ GeoSet.append(spiral)
+
+ self.rawGeoList = GeoSet
+
+ def _ZigZag(self):
+ self._Line() # Use _Line generator
+
+ # Support methods
+ def _getPatternCenter(self):
+ centerAt = self.obj.PatternCenterAt
+
+ if centerAt == 'CenterOfMass':
+ cntrPnt = FreeCAD.Vector(self.centerOfMass.x, self.centerOfMass.y, 0.0)
+ elif centerAt == 'CenterOfBoundBox':
+ cent = self.shape.BoundBox.Center
+ cntrPnt = FreeCAD.Vector(cent.x, cent.y, 0.0)
+ elif centerAt == 'XminYmin':
+ cntrPnt = FreeCAD.Vector(self.shape.BoundBox.XMin, self.shape.BoundBox.YMin, 0.0)
+ elif centerAt == 'Custom':
+ cntrPnt = FreeCAD.Vector(self.obj.PatternCenterCustom.x, self.obj.PatternCenterCustom.y, 0.0)
+
+ # Update centerOfPattern point
+ if centerAt != 'Custom':
+ self.obj.PatternCenterCustom = cntrPnt
+ self.centerOfPattern = cntrPnt
+
+ return cntrPnt
+
+ def _getRadialPasses(self):
+ # recalculate number of passes, if need be
+ radialPasses = self.halfPasses
+ if self.obj.PatternCenterAt != 'CenterOfBoundBox':
+ # make 4 corners of boundbox in XY plane, find which is greatest distance to new circular center
+ EBB = self.shape.BoundBox
+ CORNERS = [
+ FreeCAD.Vector(EBB.XMin, EBB.YMin, 0.0),
+ FreeCAD.Vector(EBB.XMin, EBB.YMax, 0.0),
+ FreeCAD.Vector(EBB.XMax, EBB.YMax, 0.0),
+ FreeCAD.Vector(EBB.XMax, EBB.YMin, 0.0),
+ ]
+ dMax = 0.0
+ for c in range(0, 4):
+ dist = CORNERS[c].sub(self.centerOfPattern).Length
+ if dist > dMax:
+ dMax = dist
+ diag = dMax + (2.0 * self.toolDiam) # Line length to span boundbox diag with 2x cutter diameter extra on each end
+ radialPasses = math.ceil(diag / self.cutOut) + 1 # Number of lines(passes) required to cover boundbox diagonal
+
+ return radialPasses
+
+ def _makeRegSpiralPnt(self, move, b, radAng):
+ x = b * radAng * math.cos(radAng)
+ y = b * radAng * math.sin(radAng)
+ return FreeCAD.Vector(x, y, 0.0).add(move)
+
+ def _makeOppSpiralPnt(self, move, b, radAng):
+ x = b * radAng * math.cos(radAng)
+ y = b * radAng * math.sin(radAng)
+ return FreeCAD.Vector(-1 * x, y, 0.0).add(move)
+
+ def _extractOffsetFaces(self):
+ PathLog.debug('_extractOffsetFaces()')
+ wires = list()
+ faces = list()
+ ofst = 0.0 # - self.cutOut
+ shape = self.shape
+ cont = True
+ cnt = 0
+ while cont:
+ ofstArea = self._getFaceOffset(shape, ofst)
+ if not ofstArea:
+ PathLog.warning('PGG: No offset clearing area returned.')
+ cont = False
+ break
+ for F in ofstArea.Faces:
+ faces.append(F)
+ for w in F.Wires:
+ wires.append(w)
+ shape = ofstArea
+ if cnt == 0:
+ ofst = 0.0 - self.cutOut
+ cnt += 1
+ return wires
+
+ def _getFaceOffset(self, shape, offset):
+ '''_getFaceOffset(shape, offset) ... internal function.
+ Original _buildPathArea() version copied from PathAreaOp.py module. This version is modified.
+ Adjustments made based on notes by @sliptonic at this webpage: https://github.com/sliptonic/FreeCAD/wiki/PathArea-notes.'''
+ PathLog.debug('_getFaceOffset()')
+
+ areaParams = {}
+ areaParams['Offset'] = offset
+ areaParams['Fill'] = 1 # 1
+ areaParams['Coplanar'] = 0
+ areaParams['SectionCount'] = 1 # -1 = full(all per depthparams??) sections
+ areaParams['Reorient'] = True
+ areaParams['OpenMode'] = 0
+ areaParams['MaxArcPoints'] = 400 # 400
+ areaParams['Project'] = True
+
+ area = Path.Area() # Create instance of Area() class object
+ # area.setPlane(PathUtils.makeWorkplane(shape)) # Set working plane
+ area.setPlane(PathUtils.makeWorkplane(self.wpc)) # Set working plane to normal at Z=1
+ area.add(shape)
+ area.setParams(**areaParams) # set parameters
+
+ offsetShape = area.getShape()
+ wCnt = len(offsetShape.Wires)
+ if wCnt == 0:
+ return False
+ elif wCnt == 1:
+ ofstFace = Part.Face(offsetShape.Wires[0])
+ else:
+ W = list()
+ for wr in offsetShape.Wires:
+ W.append(Part.Face(wr))
+ ofstFace = Part.makeCompound(W)
+
+ return ofstFace
+# Eclass
+
+
+class ProcessSelectedFaces:
+ """ProcessSelectedFaces(JOB, obj) class.
+ This class processes the `obj.Base` object for selected geometery.
+ Calling the preProcessModel(module) method returns
+ two compound objects as a tuple: (FACES, VOIDS) or False."""
+
+ def __init__(self, JOB, obj):
+ self.modelSTLs = list()
+ self.profileShapes = list()
+ self.tempGroup = False
+ self.showDebugObjects = False
+ self.checkBase = False
+ self.module = None
+ self.radius = None
+ self.depthParams = None
+ self.msgNoFaces = translate(self.module, 'Face selection is unavailable for Rotational scans. Ignoring selected faces.')
+ self.JOB = JOB
+ self.obj = obj
+ self.profileEdges = 'None'
+
+ if hasattr(obj, 'ProfileEdges'):
+ self.profileEdges = obj.ProfileEdges
+
+ # Setup STL, model type, and bound box containers for each model in Job
+ for m in range(0, len(JOB.Model.Group)):
+ M = JOB.Model.Group[m]
+ self.modelSTLs.append(False)
+ self.profileShapes.append(False)
+
+ # make circle for workplane
+ self.wpc = Part.makeCircle(2.0)
+
+ def PathSurface(self):
+ if self.obj.Base:
+ if len(self.obj.Base) > 0:
+ self.checkBase = True
+ if self.obj.ScanType == 'Rotational':
+ self.checkBase = False
+ PathLog.warning(self.msgNoFaces)
+
+ def PathWaterline(self):
+ if self.obj.Base:
+ if len(self.obj.Base) > 0:
+ self.checkBase = True
+ if self.obj.Algorithm in ['OCL Dropcutter', 'Experimental']:
+ self.checkBase = False
+ PathLog.warning(self.msgNoFaces)
+
+ # public class methods
+ def setShowDebugObjects(self, grpObj, val):
+ self.tempGroup = grpObj
+ self.showDebugObjects = val
+
+ def preProcessModel(self, module):
+ PathLog.debug('preProcessModel()')
+
+ if not self._isReady(module):
+ return False
+
+ FACES = list()
+ VOIDS = list()
+ fShapes = list()
+ vShapes = list()
+ GRP = self.JOB.Model.Group
+ lenGRP = len(GRP)
+
+ # Crete place holders for each base model in Job
+ for m in range(0, lenGRP):
+ FACES.append(False)
+ VOIDS.append(False)
+ fShapes.append(False)
+ vShapes.append(False)
+
+ # The user has selected subobjects from the base. Pre-Process each.
+ if self.checkBase:
+ PathLog.debug(' -obj.Base exists. Pre-processing for selected faces.')
+
+ # (FACES, VOIDS) = self._identifyFacesAndVoids(FACES, VOIDS)
+ (F, V) = self._identifyFacesAndVoids(FACES, VOIDS)
+
+ # Cycle through each base model, processing faces for each
+ for m in range(0, lenGRP):
+ base = GRP[m]
+ (mFS, mVS, mPS) = self._preProcessFacesAndVoids(base, m, FACES, VOIDS)
+ fShapes[m] = mFS
+ vShapes[m] = mVS
+ self.profileShapes[m] = mPS
+ else:
+ PathLog.debug(' -No obj.Base data.')
+ for m in range(0, lenGRP):
+ self.modelSTLs[m] = True
+
+ # Process each model base, as a whole, as needed
+ # PathLog.debug(' -Pre-processing all models in Job.')
+ for m in range(0, lenGRP):
+ if fShapes[m] is False:
+ PathLog.debug(' -Pre-processing {} as a whole.'.format(GRP[m].Label))
+ if self.obj.BoundBox == 'BaseBoundBox':
+ base = GRP[m]
+ elif self.obj.BoundBox == 'Stock':
+ base = self.JOB.Stock
+
+ pPEB = self._preProcessEntireBase(base, m)
+ if pPEB is False:
+ PathLog.error(' -Failed to pre-process base as a whole.')
+ else:
+ (fcShp, prflShp) = pPEB
+ if fcShp is not False:
+ if fcShp is True:
+ PathLog.debug(' -fcShp is True.')
+ fShapes[m] = True
+ else:
+ fShapes[m] = [fcShp]
+ if prflShp is not False:
+ if fcShp is not False:
+ PathLog.debug('vShapes[{}]: {}'.format(m, vShapes[m]))
+ if vShapes[m] is not False:
+ PathLog.debug(' -Cutting void from base profile shape.')
+ adjPS = prflShp.cut(vShapes[m][0])
+ self.profileShapes[m] = [adjPS]
+ else:
+ PathLog.debug(' -vShapes[m] is False.')
+ self.profileShapes[m] = [prflShp]
+ else:
+ PathLog.debug(' -Saving base profile shape.')
+ self.profileShapes[m] = [prflShp]
+ PathLog.debug('self.profileShapes[{}]: {}'.format(m, self.profileShapes[m]))
+ # Efor
+
+ return (fShapes, vShapes)
+
+ # private class methods
+ def _isReady(self, module):
+ '''_isReady(module)... Internal method.
+ Checks if required attributes are available for processing obj.Base (the Base Geometry).'''
+ if hasattr(self, module):
+ self.module = module
+ modMethod = getattr(self, module) # gets the attribute only
+ modMethod() # executes as method
+ else:
+ return False
+
+ if not self.radius:
+ return False
+
+ if not self.depthParams:
+ return False
+
+ return True
+
+ def _identifyFacesAndVoids(self, F, V):
+ TUPS = list()
+ GRP = self.JOB.Model.Group
+ lenGRP = len(GRP)
+
+ # Separate selected faces into (base, face) tuples and flag model(s) for STL creation
+ for (bs, SBS) in self.obj.Base:
+ for sb in SBS:
+ # Flag model for STL creation
+ mdlIdx = None
+ for m in range(0, lenGRP):
+ if bs is GRP[m]:
+ self.modelSTLs[m] = True
+ mdlIdx = m
+ break
+ TUPS.append((mdlIdx, bs, sb)) # (model idx, base, sub)
+
+ # Apply `AvoidXFaces` value
+ faceCnt = len(TUPS)
+ add = faceCnt - self.obj.AvoidLastX_Faces
+ for bst in range(0, faceCnt):
+ (m, base, sub) = TUPS[bst]
+ shape = getattr(base.Shape, sub)
+ if isinstance(shape, Part.Face):
+ faceIdx = int(sub[4:]) - 1
+ if bst < add:
+ if F[m] is False:
+ F[m] = list()
+ F[m].append((shape, faceIdx))
+ else:
+ if V[m] is False:
+ V[m] = list()
+ V[m].append((shape, faceIdx))
+ return (F, V)
+
+ def _preProcessFacesAndVoids(self, base, m, FACES, VOIDS):
+ mFS = False
+ mVS = False
+ mPS = False
+ mIFS = list()
+
+ if FACES[m] is not False:
+ isHole = False
+ if self.obj.HandleMultipleFeatures == 'Collectively':
+ cont = True
+ fsL = list() # face shape list
+ ifL = list() # avoid shape list
+ outFCS = list()
+
+ # Get collective envelope slice of selected faces
+ for (fcshp, fcIdx) in FACES[m]:
+ fNum = fcIdx + 1
+ fsL.append(fcshp)
+ gFW = self._getFaceWires(base, fcshp, fcIdx)
+ if gFW is False:
+ PathLog.debug('Failed to get wires from Face{}'.format(fNum))
+ elif gFW[0] is False:
+ PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum))
+ else:
+ ((otrFace, raised), intWires) = gFW
+ outFCS.append(otrFace)
+ if self.obj.InternalFeaturesCut is False:
+ if intWires is not False:
+ for (iFace, rsd) in intWires:
+ ifL.append(iFace)
+
+ PathLog.debug('Attempting to get cross-section of collective faces.')
+ if len(outFCS) == 0:
+ PathLog.error('Cannot process selected faces. Check horizontal surface exposure.'.format(fNum))
+ cont = False
+ else:
+ cfsL = Part.makeCompound(outFCS)
+
+ # Handle profile edges request
+ if cont is True and self.profileEdges != 'None':
+ ofstVal = self._calculateOffsetValue(isHole)
+ psOfst = extractFaceOffset(cfsL, ofstVal, self.wpc)
+ if psOfst is not False:
+ mPS = [psOfst]
+ if self.profileEdges == 'Only':
+ mFS = True
+ cont = False
+ else:
+ PathLog.error(' -Failed to create profile geometry for selected faces.')
+ cont = False
+
+ if cont:
+ if self.showDebugObjects:
+ T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCollectiveShape')
+ T.Shape = cfsL
+ T.purgeTouched()
+ self.tempGroup.addObject(T)
+
+ ofstVal = self._calculateOffsetValue(isHole)
+ faceOfstShp = extractFaceOffset(cfsL, ofstVal, self.wpc)
+ if faceOfstShp is False:
+ PathLog.error(' -Failed to create offset face.')
+ cont = False
+
+ if cont:
+ lenIfL = len(ifL)
+ if self.obj.InternalFeaturesCut is False:
+ if lenIfL == 0:
+ PathLog.debug(' -No internal features saved.')
+ else:
+ if lenIfL == 1:
+ casL = ifL[0]
+ else:
+ casL = Part.makeCompound(ifL)
+ if self.showDebugObjects:
+ C = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCompoundIntFeat')
+ C.Shape = casL
+ C.purgeTouched()
+ self.tempGroup.addObject(C)
+ ofstVal = self._calculateOffsetValue(isHole=True)
+ intOfstShp = extractFaceOffset(casL, ofstVal, self.wpc)
+ mIFS.append(intOfstShp)
+ # faceOfstShp = faceOfstShp.cut(intOfstShp)
+
+ mFS = [faceOfstShp]
+ # Eif
+
+ elif self.obj.HandleMultipleFeatures == 'Individually':
+ for (fcshp, fcIdx) in FACES[m]:
+ cont = True
+ ifL = list() # avoid shape list
+ fNum = fcIdx + 1
+ outerFace = False
+
+ gFW = self._getFaceWires(base, fcshp, fcIdx)
+ if gFW is False:
+ PathLog.debug('Failed to get wires from Face{}'.format(fNum))
+ cont = False
+ elif gFW[0] is False:
+ PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum))
+ cont = False
+ outerFace = False
+ else:
+ ((otrFace, raised), intWires) = gFW
+ outerFace = otrFace
+ if self.obj.InternalFeaturesCut is False:
+ if intWires is not False:
+ for (iFace, rsd) in intWires:
+ ifL.append(iFace)
+
+ if outerFace is not False:
+ PathLog.debug('Attempting to create offset face of Face{}'.format(fNum))
+
+ if self.profileEdges != 'None':
+ ofstVal = self._calculateOffsetValue(isHole)
+ psOfst = extractFaceOffset(outerFace, ofstVal, self.wpc)
+ if psOfst is not False:
+ if mPS is False:
+ mPS = list()
+ mPS.append(psOfst)
+ if self.profileEdges == 'Only':
+ if mFS is False:
+ mFS = list()
+ mFS.append(True)
+ cont = False
+ else:
+ PathLog.error(' -Failed to create profile geometry for Face{}.'.format(fNum))
+ cont = False
+
+ if cont:
+ ofstVal = self._calculateOffsetValue(isHole)
+ faceOfstShp = extractFaceOffset(outerFace, ofstVal, self.wpc)
+
+ lenIfl = len(ifL)
+ if self.obj.InternalFeaturesCut is False and lenIfl > 0:
+ if lenIfl == 1:
+ casL = ifL[0]
+ else:
+ casL = Part.makeCompound(ifL)
+
+ ofstVal = self._calculateOffsetValue(isHole=True)
+ intOfstShp = extractFaceOffset(casL, ofstVal, self.wpc)
+ mIFS.append(intOfstShp)
+ # faceOfstShp = faceOfstShp.cut(intOfstShp)
+
+ if mFS is False:
+ mFS = list()
+ mFS.append(faceOfstShp)
+ # Eif
+ # Efor
+ # Eif
+ # Eif
+
+ if len(mIFS) > 0:
+ if mVS is False:
+ mVS = list()
+ for ifs in mIFS:
+ mVS.append(ifs)
+
+ if VOIDS[m] is not False:
+ PathLog.debug('Processing avoid faces.')
+ cont = True
+ isHole = False
+ outFCS = list()
+ intFEAT = list()
+
+ for (fcshp, fcIdx) in VOIDS[m]:
+ fNum = fcIdx + 1
+ gFW = self._getFaceWires(base, fcshp, fcIdx)
+ if gFW is False:
+ PathLog.debug('Failed to get wires from avoid Face{}'.format(fNum))
+ cont = False
+ else:
+ ((otrFace, raised), intWires) = gFW
+ outFCS.append(otrFace)
+ if self.obj.AvoidLastX_InternalFeatures is False:
+ if intWires is not False:
+ for (iFace, rsd) in intWires:
+ intFEAT.append(iFace)
+
+ lenOtFcs = len(outFCS)
+ if lenOtFcs == 0:
+ cont = False
+ else:
+ if lenOtFcs == 1:
+ avoid = outFCS[0]
+ else:
+ avoid = Part.makeCompound(outFCS)
+
+ if self.showDebugObjects:
+ PathLog.debug('*** tmpAvoidArea')
+ P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidEnvelope')
+ P.Shape = avoid
+ P.purgeTouched()
+ self.tempGroup.addObject(P)
+
+ if cont:
+ if self.showDebugObjects:
+ PathLog.debug('*** tmpVoidCompound')
+ P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidCompound')
+ P.Shape = avoid
+ P.purgeTouched()
+ self.tempGroup.addObject(P)
+ ofstVal = self._calculateOffsetValue(isHole, isVoid=True)
+ avdOfstShp = extractFaceOffset(avoid, ofstVal, self.wpc)
+ if avdOfstShp is False:
+ PathLog.error('Failed to create collective offset avoid face.')
+ cont = False
+
+ if cont:
+ avdShp = avdOfstShp
+
+ if self.obj.AvoidLastX_InternalFeatures is False and len(intFEAT) > 0:
+ if len(intFEAT) > 1:
+ ifc = Part.makeCompound(intFEAT)
+ else:
+ ifc = intFEAT[0]
+ ofstVal = self._calculateOffsetValue(isHole=True)
+ ifOfstShp = extractFaceOffset(ifc, ofstVal, self.wpc)
+ if ifOfstShp is False:
+ PathLog.error('Failed to create collective offset avoid internal features.')
+ else:
+ avdShp = avdOfstShp.cut(ifOfstShp)
+
+ if mVS is False:
+ mVS = list()
+ mVS.append(avdShp)
+
+
+ return (mFS, mVS, mPS)
+
+ def _getFaceWires(self, base, fcshp, fcIdx):
+ outFace = False
+ INTFCS = list()
+ fNum = fcIdx + 1
+ warnFinDep = translate(self.module, 'Final Depth might need to be lower. Internal features detected in Face')
+
+ PathLog.debug('_getFaceWires() from Face{}'.format(fNum))
+ WIRES = self._extractWiresFromFace(base, fcshp)
+ if WIRES is False:
+ PathLog.error('Failed to extract wires from Face{}'.format(fNum))
+ return False
+
+ # Process remaining internal features, adding to FCS list
+ lenW = len(WIRES)
+ for w in range(0, lenW):
+ (wire, rsd) = WIRES[w]
+ PathLog.debug('Processing Wire{} in Face{}. isRaised: {}'.format(w + 1, fNum, rsd))
+ if wire.isClosed() is False:
+ PathLog.debug(' -wire is not closed.')
+ else:
+ slc = self._flattenWireToFace(wire)
+ if slc is False:
+ PathLog.error('FAILED to identify horizontal exposure on Face{}.'.format(fNum))
+ else:
+ if w == 0:
+ outFace = (slc, rsd)
+ else:
+ # add to VOIDS so cutter avoids area.
+ PathLog.warning(warnFinDep + str(fNum) + '.')
+ INTFCS.append((slc, rsd))
+ if len(INTFCS) == 0:
+ return (outFace, False)
+ else:
+ return (outFace, INTFCS)
+
+ def _preProcessEntireBase(self, base, m):
+ cont = True
+ isHole = False
+ prflShp = False
+ # Create envelope, extract cross-section and make offset co-planar shape
+ # baseEnv = PathUtils.getEnvelope(base.Shape, subshape=None, depthparams=self.depthParams)
+
+ try:
+ baseEnv = PathUtils.getEnvelope(partshape=base.Shape, subshape=None, depthparams=self.depthParams) # Produces .Shape
+ except Exception as ee:
+ PathLog.error(str(ee))
+ shell = base.Shape.Shells[0]
+ solid = Part.makeSolid(shell)
+ try:
+ baseEnv = PathUtils.getEnvelope(partshape=solid, subshape=None, depthparams=self.depthParams) # Produces .Shape
+ except Exception as eee:
+ PathLog.error(str(eee))
+ cont = False
+
+ if cont:
+ csFaceShape = getShapeSlice(baseEnv)
+ if csFaceShape is False:
+ PathLog.debug('getShapeSlice(baseEnv) failed')
+ csFaceShape = getCrossSection(baseEnv)
+ if csFaceShape is False:
+ PathLog.debug('getCrossSection(baseEnv) failed')
+ csFaceShape = getSliceFromEnvelope(baseEnv)
+ if csFaceShape is False:
+ PathLog.error('Failed to slice baseEnv shape.')
+ cont = False
+
+ if cont is True and self.profileEdges != 'None':
+ PathLog.debug(' -Attempting profile geometry for model base.')
+ ofstVal = self._calculateOffsetValue(isHole)
+ psOfst = extractFaceOffset(csFaceShape, ofstVal, self.wpc)
+ if psOfst is not False:
+ if self.profileEdges == 'Only':
+ return (True, psOfst)
+ prflShp = psOfst
+ else:
+ PathLog.error(' -Failed to create profile geometry.')
+ cont = False
+
+ if cont:
+ ofstVal = self._calculateOffsetValue(isHole)
+ faceOffsetShape = extractFaceOffset(csFaceShape, ofstVal, self.wpc)
+ if faceOffsetShape is False:
+ PathLog.error('extractFaceOffset() failed.')
+ else:
+ faceOffsetShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - faceOffsetShape.BoundBox.ZMin))
+ return (faceOffsetShape, prflShp)
+ return False
+
+ def _extractWiresFromFace(self, base, fc):
+ '''_extractWiresFromFace(base, fc) ...
+ Attempts to return all closed wires within a parent face, including the outer most wire of the parent.
+ The wires are ordered by area. Each wire is also categorized as a pocket(False) or raised protrusion(True).
+ '''
+ PathLog.debug('_extractWiresFromFace()')
+
+ WIRES = list()
+ lenWrs = len(fc.Wires)
+ PathLog.debug(' -Wire count: {}'.format(lenWrs))
+
+ def index0(tup):
+ return tup[0]
+
+ # Cycle through wires in face
+ for w in range(0, lenWrs):
+ PathLog.debug(' -Analyzing wire_{}'.format(w + 1))
+ wire = fc.Wires[w]
+ checkEdges = False
+ cont = True
+
+ # Check for closed edges (circles, ellipses, etc...)
+ for E in wire.Edges:
+ if E.isClosed() is True:
+ checkEdges = True
+ break
+
+ if checkEdges is True:
+ PathLog.debug(' -checkEdges is True')
+ for e in range(0, len(wire.Edges)):
+ edge = wire.Edges[e]
+ if edge.isClosed() is True and edge.Mass > 0.01:
+ PathLog.debug(' -Found closed edge')
+ raised = False
+ ip = self._isPocket(base, fc, edge)
+ if ip is False:
+ raised = True
+ ebb = edge.BoundBox
+ eArea = ebb.XLength * ebb.YLength
+ F = Part.Face(Part.Wire([edge]))
+ WIRES.append((eArea, F.Wires[0], raised))
+ cont = False
+
+ if cont:
+ PathLog.debug(' -cont is True')
+ # If only one wire and not checkEdges, return first wire
+ if lenWrs == 1:
+ return [(wire, False)]
+
+ raised = False
+ wbb = wire.BoundBox
+ wArea = wbb.XLength * wbb.YLength
+ if w > 0:
+ ip = self._isPocket(base, fc, wire)
+ if ip is False:
+ raised = True
+ WIRES.append((wArea, Part.Wire(wire.Edges), raised))
+
+ nf = len(WIRES)
+ if nf > 0:
+ PathLog.debug(' -number of wires found is {}'.format(nf))
+ if nf == 1:
+ (area, W, raised) = WIRES[0]
+ owLen = fc.OuterWire.Length
+ wLen = W.Length
+ if abs(owLen - wLen) > 0.0000001:
+ OW = Part.Wire(Part.__sortEdges__(fc.OuterWire.Edges))
+ return [(OW, False), (W, raised)]
+ else:
+ return [(W, raised)]
+ else:
+ sortedWIRES = sorted(WIRES, key=index0, reverse=True)
+ WRS = [(W, raised) for (area, W, raised) in sortedWIRES] # outer, then inner by area size
+ # Check if OuterWire is larger than largest in WRS list
+ (W, raised) = WRS[0]
+ owLen = fc.OuterWire.Length
+ wLen = W.Length
+ if abs(owLen - wLen) > 0.0000001:
+ OW = Part.Wire(Part.__sortEdges__(fc.OuterWire.Edges))
+ WRS.insert(0, (OW, False))
+ return WRS
+
+ return False
+
+ def _calculateOffsetValue(self, isHole, isVoid=False):
+ '''_calculateOffsetValue(self.obj, isHole, isVoid) ... internal function.
+ Calculate the offset for the Path.Area() function.'''
+ self.JOB = PathUtils.findParentJob(self.obj)
+ tolrnc = self.JOB.GeometryTolerance.Value
+
+ if isVoid is False:
+ if isHole is True:
+ offset = -1 * self.obj.InternalFeaturesAdjustment.Value
+ offset += self.radius + (tolrnc / 10.0)
+ else:
+ offset = -1 * self.obj.BoundaryAdjustment.Value
+ if self.obj.BoundaryEnforcement is True:
+ offset += self.radius + (tolrnc / 10.0)
+ else:
+ offset -= self.radius + (tolrnc / 10.0)
+ offset = 0.0 - offset
+ else:
+ offset = -1 * self.obj.BoundaryAdjustment.Value
+ offset += self.radius + (tolrnc / 10.0)
+
+ return offset
+
+ def _isPocket(self, b, f, w):
+ '''_isPocket(b, f, w)...
+ Attempts to determine if the wire(w) in face(f) of base(b) is a pocket or raised protrusion.
+ Returns True if pocket, False if raised protrusion.'''
+ e = w.Edges[0]
+ for fi in range(0, len(b.Shape.Faces)):
+ face = b.Shape.Faces[fi]
+ for ei in range(0, len(face.Edges)):
+ edge = face.Edges[ei]
+ if e.isSame(edge) is True:
+ if f is face:
+ # Alternative: run loop to see if all edges are same
+ pass # same source face, look for another
+ else:
+ if face.CenterOfMass.z < f.CenterOfMass.z:
+ return True
+ return False
+
+ def _flattenWireToFace(self, wire):
+ PathLog.debug('_flattenWireToFace()')
+ if wire.isClosed() is False:
+ PathLog.debug(' -wire.isClosed() is False')
+ return False
+
+ # If wire is planar horizontal, convert to a face and return
+ if wire.BoundBox.ZLength == 0.0:
+ slc = Part.Face(wire)
+ return slc
+
+ # Attempt to create a new wire for manipulation, if not, use original
+ newWire = Part.Wire(wire.Edges)
+ if newWire.isClosed() is True:
+ nWire = newWire
+ else:
+ PathLog.debug(' -newWire.isClosed() is False')
+ nWire = wire
+
+ # Attempt extrusion, and then try a manual slice and then cross-section
+ ext = getExtrudedShape(nWire)
+ if ext is False:
+ PathLog.debug('getExtrudedShape() failed')
+ else:
+ slc = getShapeSlice(ext)
+ if slc is not False:
+ return slc
+ cs = getCrossSection(ext, True)
+ if cs is not False:
+ return cs
+
+ # Attempt creating an envelope, and then try a manual slice and then cross-section
+ env = getShapeEnvelope(nWire)
+ if env is False:
+ PathLog.debug('getShapeEnvelope() failed')
+ else:
+ slc = getShapeSlice(env)
+ if slc is not False:
+ return slc
+ cs = getCrossSection(env, True)
+ if cs is not False:
+ return cs
+
+ # Attempt creating a projection
+ slc = getProjectedFace(self.tempGroup, nWire)
+ if slc is False:
+ PathLog.debug('getProjectedFace() failed')
+ else:
+ return slc
+
+ return False
+# Eclass
+
+
+# Functions for getting a shape envelope and cross-section
+def getExtrudedShape(wire):
+ PathLog.debug('getExtrudedShape()')
+ wBB = wire.BoundBox
+ extFwd = math.floor(2.0 * wBB.ZLength) + 10.0
+
+ try:
+ shell = wire.extrude(FreeCAD.Vector(0.0, 0.0, extFwd))
+ except Exception as ee:
+ PathLog.error(' -extrude wire failed: \n{}'.format(ee))
+ return False
+
+ SHP = Part.makeSolid(shell)
+ return SHP
+
+def getShapeSlice(shape):
+ PathLog.debug('getShapeSlice()')
+
+ bb = shape.BoundBox
+ mid = (bb.ZMin + bb.ZMax) / 2.0
+ xmin = bb.XMin - 1.0
+ xmax = bb.XMax + 1.0
+ ymin = bb.YMin - 1.0
+ ymax = bb.YMax + 1.0
+ p1 = FreeCAD.Vector(xmin, ymin, mid)
+ p2 = FreeCAD.Vector(xmax, ymin, mid)
+ p3 = FreeCAD.Vector(xmax, ymax, mid)
+ p4 = FreeCAD.Vector(xmin, ymax, mid)
+
+ e1 = Part.makeLine(p1, p2)
+ e2 = Part.makeLine(p2, p3)
+ e3 = Part.makeLine(p3, p4)
+ e4 = Part.makeLine(p4, p1)
+ face = Part.Face(Part.Wire([e1, e2, e3, e4]))
+ fArea = face.BoundBox.XLength * face.BoundBox.YLength # face.Wires[0].Area
+ sArea = shape.BoundBox.XLength * shape.BoundBox.YLength
+ midArea = (fArea + sArea) / 2.0
+
+ slcShp = shape.common(face)
+ slcArea = slcShp.BoundBox.XLength * slcShp.BoundBox.YLength
+
+ if slcArea < midArea:
+ for W in slcShp.Wires:
+ if W.isClosed() is False:
+ PathLog.debug(' -wire.isClosed() is False')
+ return False
+ if len(slcShp.Wires) == 1:
+ wire = slcShp.Wires[0]
+ slc = Part.Face(wire)
+ slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin))
+ return slc
+ else:
+ fL = list()
+ for W in slcShp.Wires:
+ slc = Part.Face(W)
+ slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin))
+ fL.append(slc)
+ comp = Part.makeCompound(fL)
+ return comp
+
+ # PathLog.debug(' -slcArea !< midArea')
+ # PathLog.debug(' -slcShp.Edges count: {}. Might be a vertically oriented face.'.format(len(slcShp.Edges)))
+ return False
+
+def getProjectedFace(tempGroup, wire):
+ import Draft
+ PathLog.debug('getProjectedFace()')
+ F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpProjectionWire')
+ F.Shape = wire
+ F.purgeTouched()
+ tempGroup.addObject(F)
+ try:
+ prj = Draft.makeShape2DView(F, FreeCAD.Vector(0, 0, 1))
+ prj.recompute()
+ prj.purgeTouched()
+ tempGroup.addObject(prj)
+ except Exception as ee:
+ PathLog.error(str(ee))
+ return False
+ else:
+ pWire = Part.Wire(prj.Shape.Edges)
+ if pWire.isClosed() is False:
+ # PathLog.debug(' -pWire.isClosed() is False')
+ return False
+ slc = Part.Face(pWire)
+ slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin))
+ return slc
+
+def getCrossSection(shape, withExtrude=False):
+ PathLog.debug('getCrossSection()')
+ wires = list()
+ bb = shape.BoundBox
+ mid = (bb.ZMin + bb.ZMax) / 2.0
+
+ for i in shape.slice(FreeCAD.Vector(0, 0, 1), mid):
+ wires.append(i)
+
+ if len(wires) > 0:
+ comp = Part.Compound(wires) # produces correct cross-section wire !
+ comp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - comp.BoundBox.ZMin))
+ csWire = comp.Wires[0]
+ if csWire.isClosed() is False:
+ PathLog.debug(' -comp.Wires[0] is not closed')
+ return False
+ if withExtrude is True:
+ ext = getExtrudedShape(csWire)
+ CS = getShapeSlice(ext)
+ if CS is False:
+ return False
+ else:
+ CS = Part.Face(csWire)
+ CS.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - CS.BoundBox.ZMin))
+ return CS
+ else:
+ PathLog.debug(' -No wires from .slice() method')
+
+ return False
+
+def getShapeEnvelope(shape):
+ PathLog.debug('getShapeEnvelope()')
+
+ wBB = shape.BoundBox
+ extFwd = wBB.ZLength + 10.0
+ minz = wBB.ZMin
+ maxz = wBB.ZMin + extFwd
+ stpDwn = (maxz - minz) / 4.0
+ dep_par = PathUtils.depth_params(maxz + 5.0, maxz + 3.0, maxz, stpDwn, 0.0, minz)
+
+ try:
+ env = PathUtils.getEnvelope(partshape=shape, depthparams=dep_par) # Produces .Shape
+ except Exception as ee:
+ PathLog.error('try: PathUtils.getEnvelope() failed.\n' + str(ee))
+ return False
+ else:
+ return env
+
+def getSliceFromEnvelope(env):
+ PathLog.debug('getSliceFromEnvelope()')
+ eBB = env.BoundBox
+ extFwd = eBB.ZLength + 10.0
+ maxz = eBB.ZMin + extFwd
+
+ emax = math.floor(maxz - 1.0)
+ E = list()
+ for e in range(0, len(env.Edges)):
+ emin = env.Edges[e].BoundBox.ZMin
+ if emin > emax:
+ E.append(env.Edges[e])
+ tf = Part.Face(Part.Wire(Part.__sortEdges__(E)))
+ tf.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - tf.BoundBox.ZMin))
+
+ return tf
+
+
+# Function to extract offset face from shape
+def extractFaceOffset(fcShape, offset, wpc, makeComp=True):
+ '''extractFaceOffset(fcShape, offset) ... internal function.
+ Original _buildPathArea() version copied from PathAreaOp.py module. This version is modified.
+ Adjustments made based on notes by @sliptonic at this webpage: https://github.com/sliptonic/FreeCAD/wiki/PathArea-notes.'''
+ PathLog.debug('extractFaceOffset()')
+
+ if fcShape.BoundBox.ZMin != 0.0:
+ fcShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - fcShape.BoundBox.ZMin))
+
+ areaParams = {}
+ areaParams['Offset'] = offset
+ areaParams['Fill'] = 1 # 1
+ areaParams['Coplanar'] = 0
+ areaParams['SectionCount'] = 1 # -1 = full(all per depthparams??) sections
+ areaParams['Reorient'] = True
+ areaParams['OpenMode'] = 0
+ areaParams['MaxArcPoints'] = 400 # 400
+ areaParams['Project'] = True
+
+ area = Path.Area() # Create instance of Area() class object
+ # area.setPlane(PathUtils.makeWorkplane(fcShape)) # Set working plane
+ area.setPlane(PathUtils.makeWorkplane(wpc)) # Set working plane to normal at Z=1
+ area.add(fcShape)
+ area.setParams(**areaParams) # set parameters
+
+ offsetShape = area.getShape()
+ wCnt = len(offsetShape.Wires)
+ if wCnt == 0:
+ return False
+ elif wCnt == 1:
+ ofstFace = Part.Face(offsetShape.Wires[0])
+ if not makeComp:
+ ofstFace = [ofstFace]
+ else:
+ W = list()
+ for wr in offsetShape.Wires:
+ W.append(Part.Face(wr))
+ if makeComp:
+ ofstFace = Part.makeCompound(W)
+ else:
+ ofstFace = W
+
+ return ofstFace # offsetShape
+
+
+# Functions to convert path geometry into line/arc segments for OCL input or directly to g-code
+def pathGeomToLinesPointSet(obj, compGeoShp, cutClimb, toolDiam, closedGap, gaps):
+ '''pathGeomToLinesPointSet(obj, compGeoShp)...
+ Convert a compound set of sequential line segments to directionally-oriented collinear groupings.'''
+ PathLog.debug('pathGeomToLinesPointSet()')
+ # Extract intersection line segments for return value as list()
+ LINES = list()
+ inLine = list()
+ chkGap = False
+ lnCnt = 0
+ ec = len(compGeoShp.Edges)
+ cpa = obj.CutPatternAngle
+
+ edg0 = compGeoShp.Edges[0]
+ p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y)
+ p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y)
+ if cutClimb is True:
+ tup = (p2, p1)
+ lst = FreeCAD.Vector(p1[0], p1[1], 0.0)
+ else:
+ tup = (p1, p2)
+ lst = FreeCAD.Vector(p2[0], p2[1], 0.0)
+ inLine.append(tup)
+ sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point
+
+ for ei in range(1, ec):
+ chkGap = False
+ edg = compGeoShp.Edges[ei] # Get edge for vertexes
+ v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y) # vertex 0
+ v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y) # vertex 1
+
+ ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point
+ cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (first / middle point)
+ # iC = sp.isOnLineSegment(ep, cp)
+ iC = cp.isOnLineSegment(sp, ep)
+ if iC is True:
+ inLine.append('BRK')
+ chkGap = True
+ else:
+ if cutClimb is True:
+ inLine.reverse()
+ LINES.append(inLine) # Save inLine segments
+ lnCnt += 1
+ inLine = list() # reset collinear container
+ if cutClimb is True:
+ sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0)
+ else:
+ sp = ep
+
+ if cutClimb is True:
+ tup = (v2, v1)
+ if chkGap is True:
+ gap = abs(toolDiam - lst.sub(ep).Length)
+ lst = cp
+ else:
+ tup = (v1, v2)
+ if chkGap is True:
+ gap = abs(toolDiam - lst.sub(cp).Length)
+ lst = ep
+
+ if chkGap is True:
+ if gap < obj.GapThreshold.Value:
+ b = inLine.pop() # pop off 'BRK' marker
+ (vA, vB) = inLine.pop() # pop off previous line segment for combining with current
+ tup = (vA, tup[1])
+ closedGap = True
+ else:
+ # PathLog.debug('---- Gap: {} mm'.format(gap))
+ gap = round(gap, 6)
+ if gap < gaps[0]:
+ gaps.insert(0, gap)
+ gaps.pop()
+ inLine.append(tup)
+ # Efor
+ lnCnt += 1
+ if cutClimb is True:
+ inLine.reverse()
+ LINES.append(inLine) # Save inLine segments
+
+ # Handle last inLine set, reversing it.
+ if obj.CutPatternReversed is True:
+ if cpa != 0.0 and cpa % 90.0 == 0.0:
+ F = LINES.pop(0)
+ rev = list()
+ for iL in F:
+ if iL == 'BRK':
+ rev.append(iL)
+ else:
+ (p1, p2) = iL
+ rev.append((p2, p1))
+ rev.reverse()
+ LINES.insert(0, rev)
+
+ isEven = lnCnt % 2
+ if isEven == 0:
+ PathLog.debug('Line count is ODD.')
+ else:
+ PathLog.debug('Line count is even.')
+
+ return LINES
+
+def pathGeomToZigzagPointSet(obj, compGeoShp, cutClimb, toolDiam, closedGap, gaps):
+ '''_pathGeomToZigzagPointSet(obj, compGeoShp)...
+ Convert a compound set of sequential line segments to directionally-oriented collinear groupings
+ with a ZigZag directional indicator included for each collinear group.'''
+ PathLog.debug('_pathGeomToZigzagPointSet()')
+ # Extract intersection line segments for return value as list()
+ LINES = list()
+ inLine = list()
+ lnCnt = 0
+ chkGap = False
+ ec = len(compGeoShp.Edges)
+
+ if cutClimb is True:
+ dirFlg = -1
+ else:
+ dirFlg = 1
+
+ edg0 = compGeoShp.Edges[0]
+ p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y)
+ p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y)
+ if dirFlg == 1:
+ tup = (p1, p2)
+ lst = FreeCAD.Vector(p2[0], p2[1], 0.0)
+ sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point
+ else:
+ tup = (p2, p1)
+ lst = FreeCAD.Vector(p1[0], p1[1], 0.0)
+ sp = FreeCAD.Vector(p2[0], p2[1], 0.0) # start point
+ inLine.append(tup)
+
+ for ei in range(1, ec):
+ edg = compGeoShp.Edges[ei]
+ v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y)
+ v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y)
+
+ cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (start point of segment)
+ ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point
+ # iC = sp.isOnLineSegment(ep, cp)
+ iC = cp.isOnLineSegment(sp, ep)
+ if iC is True:
+ inLine.append('BRK')
+ chkGap = True
+ gap = abs(toolDiam - lst.sub(cp).Length)
+ else:
+ chkGap = False
+ if dirFlg == -1:
+ inLine.reverse()
+ # LINES.append((dirFlg, inLine))
+ LINES.append(inLine)
+ lnCnt += 1
+ dirFlg = -1 * dirFlg # Change zig to zag
+ inLine = list() # reset collinear container
+ sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0)
+
+ lst = ep
+ if dirFlg == 1:
+ tup = (v1, v2)
+ else:
+ tup = (v2, v1)
+
+ if chkGap is True:
+ if gap < obj.GapThreshold.Value:
+ b = inLine.pop() # pop off 'BRK' marker
+ (vA, vB) = inLine.pop() # pop off previous line segment for combining with current
+ if dirFlg == 1:
+ tup = (vA, tup[1])
+ else:
+ tup = (tup[0], vB)
+ closedGap = True
+ else:
+ gap = round(gap, 6)
+ if gap < gaps[0]:
+ gaps.insert(0, gap)
+ gaps.pop()
+ inLine.append(tup)
+ # Efor
+ lnCnt += 1
+
+ # Fix directional issue with LAST line when line count is even
+ isEven = lnCnt % 2
+ if isEven == 0: # Changed to != with 90 degree CutPatternAngle
+ PathLog.debug('Line count is even.')
+ else:
+ PathLog.debug('Line count is ODD.')
+ dirFlg = -1 * dirFlg
+ if obj.CutPatternReversed is False:
+ if cutClimb is True:
+ dirFlg = -1 * dirFlg
+
+ if obj.CutPatternReversed:
+ dirFlg = -1 * dirFlg
+
+ # Handle last inLine list
+ if dirFlg == 1:
+ rev = list()
+ for iL in inLine:
+ if iL == 'BRK':
+ rev.append(iL)
+ else:
+ (p1, p2) = iL
+ rev.append((p2, p1))
+
+ if not obj.CutPatternReversed:
+ rev.reverse()
+ else:
+ rev2 = list()
+ for iL in rev:
+ if iL == 'BRK':
+ rev2.append(iL)
+ else:
+ (p1, p2) = iL
+ rev2.append((p2, p1))
+ rev2.reverse()
+ rev = rev2
+
+ # LINES.append((dirFlg, rev))
+ LINES.append(rev)
+ else:
+ # LINES.append((dirFlg, inLine))
+ LINES.append(inLine)
+
+ return LINES
+
+def pathGeomToCircularPointSet(obj, compGeoShp, cutClimb, toolDiam, closedGap, gaps, COM):
+ '''pathGeomToCircularPointSet(obj, compGeoShp)...
+ Convert a compound set of arcs/circles to a set of directionally-oriented arc end points
+ and the corresponding center point.'''
+ # Extract intersection line segments for return value as list()
+ PathLog.debug('pathGeomToCircularPointSet()')
+ ARCS = list()
+ stpOvrEI = list()
+ segEI = list()
+ isSame = False
+ sameRad = None
+ ec = len(compGeoShp.Edges)
+
+ def gapDist(sp, ep):
+ X = (ep[0] - sp[0])**2
+ Y = (ep[1] - sp[1])**2
+ return math.sqrt(X + Y) # the 'z' value is zero in both points
+
+ # Separate arc data into Loops and Arcs
+ for ei in range(0, ec):
+ edg = compGeoShp.Edges[ei]
+ if edg.Closed is True:
+ stpOvrEI.append(('L', ei, False))
+ else:
+ if isSame is False:
+ segEI.append(ei)
+ isSame = True
+ pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0)
+ sameRad = pnt.sub(COM).Length
+ else:
+ # Check if arc is co-radial to current SEGS
+ pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0)
+ if abs(sameRad - pnt.sub(COM).Length) > 0.00001:
+ isSame = False
+
+ if isSame is True:
+ segEI.append(ei)
+ else:
+ # Move co-radial arc segments
+ stpOvrEI.append(['A', segEI, False])
+ # Start new list of arc segments
+ segEI = [ei]
+ isSame = True
+ pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0)
+ sameRad = pnt.sub(COM).Length
+ # Process trailing `segEI` data, if available
+ if isSame is True:
+ stpOvrEI.append(['A', segEI, False])
+
+ # Identify adjacent arcs with y=0 start/end points that connect
+ for so in range(0, len(stpOvrEI)):
+ SO = stpOvrEI[so]
+ if SO[0] == 'A':
+ startOnAxis = list()
+ endOnAxis = list()
+ EI = SO[1] # list of corresponding compGeoShp.Edges indexes
+
+ # Identify startOnAxis and endOnAxis arcs
+ for i in range(0, len(EI)):
+ ei = EI[i] # edge index
+ E = compGeoShp.Edges[ei] # edge object
+ if abs(COM.y - E.Vertexes[0].Y) < 0.00001:
+ startOnAxis.append((i, ei, E.Vertexes[0]))
+ elif abs(COM.y - E.Vertexes[1].Y) < 0.00001:
+ endOnAxis.append((i, ei, E.Vertexes[1]))
+
+ # Look for connections between startOnAxis and endOnAxis arcs. Consolidate data when connected
+ lenSOA = len(startOnAxis)
+ lenEOA = len(endOnAxis)
+ if lenSOA > 0 and lenEOA > 0:
+ for soa in range(0, lenSOA):
+ (iS, eiS, vS) = startOnAxis[soa]
+ for eoa in range(0, len(endOnAxis)):
+ (iE, eiE, vE) = endOnAxis[eoa]
+ dist = vE.X - vS.X
+ if abs(dist) < 0.00001: # They connect on axis at same radius
+ SO[2] = (eiE, eiS)
+ break
+ elif dist > 0:
+ break # stop searching
+ # Eif
+ # Eif
+ # Efor
+
+ # Construct arc data tuples for OCL
+ dirFlg = 1
+ if not cutClimb: # True yields Climb when set to Conventional
+ dirFlg = -1
+
+ # Cycle through stepOver data
+ for so in range(0, len(stpOvrEI)):
+ SO = stpOvrEI[so]
+ if SO[0] == 'L': # L = Loop/Ring/Circle
+ # PathLog.debug("SO[0] == 'Loop'")
+ lei = SO[1] # loop Edges index
+ v1 = compGeoShp.Edges[lei].Vertexes[0]
+
+ # space = obj.SampleInterval.Value / 10.0
+ # space = 0.000001
+ space = toolDiam * 0.005 # If too small, OCL will fail to scan the loop
+
+ # p1 = FreeCAD.Vector(v1.X, v1.Y, v1.Z)
+ p1 = FreeCAD.Vector(v1.X, v1.Y, 0.0) # z=0.0 for waterline; z=v1.Z for 3D Surface
+ rad = p1.sub(COM).Length
+ spcRadRatio = space/rad
+ if spcRadRatio < 1.0:
+ tolrncAng = math.asin(spcRadRatio)
+ else:
+ tolrncAng = 0.99999998 * math.pi
+ EX = COM.x + (rad * math.cos(tolrncAng))
+ EY = v1.Y - space # rad * math.sin(tolrncAng)
+
+ sp = (v1.X, v1.Y, 0.0)
+ ep = (EX, EY, 0.0)
+ cp = (COM.x, COM.y, 0.0)
+ if dirFlg == 1:
+ arc = (sp, ep, cp)
+ else:
+ arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction))
+ ARCS.append(('L', dirFlg, [arc]))
+ else: # SO[0] == 'A' A = Arc
+ # PathLog.debug("SO[0] == 'Arc'")
+ PRTS = list()
+ EI = SO[1] # list of corresponding Edges indexes
+ CONN = SO[2] # list of corresponding connected edges tuples (iE, iS)
+ chkGap = False
+ lst = None
+
+ if CONN is not False:
+ (iE, iS) = CONN
+ v1 = compGeoShp.Edges[iE].Vertexes[0]
+ v2 = compGeoShp.Edges[iS].Vertexes[1]
+ sp = (v1.X, v1.Y, 0.0)
+ ep = (v2.X, v2.Y, 0.0)
+ cp = (COM.x, COM.y, 0.0)
+ if dirFlg == 1:
+ arc = (sp, ep, cp)
+ lst = ep
+ else:
+ arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction))
+ lst = sp
+ PRTS.append(arc)
+ # Pop connected edge index values from arc segments index list
+ iEi = EI.index(iE)
+ iSi = EI.index(iS)
+ if iEi > iSi:
+ EI.pop(iEi)
+ EI.pop(iSi)
+ else:
+ EI.pop(iSi)
+ EI.pop(iEi)
+ if len(EI) > 0:
+ PRTS.append('BRK')
+ chkGap = True
+ cnt = 0
+ for ei in EI:
+ if cnt > 0:
+ PRTS.append('BRK')
+ chkGap = True
+ v1 = compGeoShp.Edges[ei].Vertexes[0]
+ v2 = compGeoShp.Edges[ei].Vertexes[1]
+ sp = (v1.X, v1.Y, 0.0)
+ ep = (v2.X, v2.Y, 0.0)
+ cp = (COM.x, COM.y, 0.0)
+ if dirFlg == 1:
+ arc = (sp, ep, cp)
+ if chkGap is True:
+ gap = abs(toolDiam - gapDist(lst, sp)) # abs(toolDiam - lst.sub(sp).Length)
+ lst = ep
+ else:
+ arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction))
+ if chkGap is True:
+ gap = abs(toolDiam - gapDist(lst, ep)) # abs(toolDiam - lst.sub(ep).Length)
+ lst = sp
+ if chkGap is True:
+ if gap < obj.GapThreshold.Value:
+ PRTS.pop() # pop off 'BRK' marker
+ (vA, vB, vC) = PRTS.pop() # pop off previous arc segment for combining with current
+ arc = (vA, arc[1], vC)
+ closedGap = True
+ else:
+ # PathLog.debug('---- Gap: {} mm'.format(gap))
+ gap = round(gap, 6)
+ if gap < gaps[0]:
+ gaps.insert(0, gap)
+ gaps.pop()
+ PRTS.append(arc)
+ cnt += 1
+
+ if dirFlg == -1:
+ PRTS.reverse()
+
+ ARCS.append(('A', dirFlg, PRTS))
+ # Eif
+ if obj.CutPattern == 'CircularZigZag':
+ dirFlg = -1 * dirFlg
+ # Efor
+
+ return ARCS
+
+def pathGeomToSpiralPointSet(obj, compGeoShp):
+ '''_pathGeomToSpiralPointSet(obj, compGeoShp)...
+ Convert a compound set of sequential line segments to directional, connected groupings.'''
+ PathLog.debug('_pathGeomToSpiralPointSet()')
+ # Extract intersection line segments for return value as list()
+ LINES = list()
+ inLine = list()
+ lnCnt = 0
+ ec = len(compGeoShp.Edges)
+ start = 2
+
+ if obj.CutPatternReversed:
+ edg1 = compGeoShp.Edges[0] # Skip first edge, as it is the closing edge: center to outer tail
+ ec -= 1
+ start = 1
+ else:
+ edg1 = compGeoShp.Edges[1] # Skip first edge, as it is the closing edge: center to outer tail
+ p1 = FreeCAD.Vector(edg1.Vertexes[0].X, edg1.Vertexes[0].Y, 0.0)
+ p2 = FreeCAD.Vector(edg1.Vertexes[1].X, edg1.Vertexes[1].Y, 0.0)
+ tup = ((p1.x, p1.y), (p2.x, p2.y))
+ inLine.append(tup)
+ lst = p2
+
+ for ei in range(start, ec): # Skipped first edge, started with second edge above as edg1
+ edg = compGeoShp.Edges[ei] # Get edge for vertexes
+ sp = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) # check point (first / middle point)
+ ep = FreeCAD.Vector(edg.Vertexes[1].X, edg.Vertexes[1].Y, 0.0) # end point
+ tup = ((sp.x, sp.y), (ep.x, ep.y))
+
+ if sp.sub(p2).Length < 0.000001:
+ inLine.append(tup)
+ else:
+ LINES.append(inLine) # Save inLine segments
+ lnCnt += 1
+ inLine = list() # reset container
+ inLine.append(tup)
+ p1 = sp
+ p2 = ep
+ # Efor
+
+ lnCnt += 1
+ LINES.append(inLine) # Save inLine segments
+
+ return LINES
+
+def pathGeomToOffsetPointSet(obj, compGeoShp):
+ '''pathGeomToOffsetPointSet(obj, compGeoShp)...
+ Convert a compound set of 3D profile segmented wires to 2D segments, applying linear optimization.'''
+ PathLog.debug('pathGeomToOffsetPointSet()')
+
+ LINES = list()
+ optimize = obj.OptimizeLinearPaths
+ ofstCnt = len(compGeoShp)
+
+ # Cycle through offeset loops
+ for ei in range(0, ofstCnt):
+ OS = compGeoShp[ei]
+ lenOS = len(OS)
+
+ if ei > 0:
+ LINES.append('BRK')
+
+ fp = FreeCAD.Vector(OS[0].x, OS[0].y, OS[0].z)
+ OS.append(fp)
+
+ # Cycle through points in each loop
+ prev = OS[0]
+ pnt = OS[1]
+ for v in range(1, lenOS):
+ nxt = OS[v + 1]
+ if optimize:
+ # iPOL = prev.isOnLineSegment(nxt, pnt)
+ iPOL = pnt.isOnLineSegment(prev, nxt)
+ if iPOL:
+ pnt = nxt
+ else:
+ tup = ((prev.x, prev.y), (pnt.x, pnt.y))
+ LINES.append(tup)
+ prev = pnt
+ pnt = nxt
+ else:
+ tup = ((prev.x, prev.y), (pnt.x, pnt.y))
+ LINES.append(tup)
+ prev = pnt
+ pnt = nxt
+ if iPOL:
+ tup = ((prev.x, prev.y), (pnt.x, pnt.y))
+ LINES.append(tup)
+ # Efor
+
+ return [LINES]
\ No newline at end of file
diff --git a/src/Mod/Path/PathScripts/PathWaterline.py b/src/Mod/Path/PathScripts/PathWaterline.py
index c1c8b66cb6..ba930db881 100644
--- a/src/Mod/Path/PathScripts/PathWaterline.py
+++ b/src/Mod/Path/PathScripts/PathWaterline.py
@@ -1,3484 +1,1971 @@
-# -*- coding: utf-8 -*-
-
-# ***************************************************************************
-# * *
-# * Copyright (c) 2019 Russell Johnson (russ4262) *
-# * Copyright (c) 2019 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 *
-# * *
-# ***************************************************************************
-
-from __future__ import print_function
-
-import FreeCAD
-import Path
-import PathScripts.PathLog as PathLog
-import PathScripts.PathUtils as PathUtils
-import PathScripts.PathOp as PathOp
-
-from PySide import QtCore
-import time
-import math
-
-# lazily loaded modules
-from lazy_loader.lazy_loader import LazyLoader
-MeshPart = LazyLoader('MeshPart', globals(), 'MeshPart')
-Draft = LazyLoader('Draft', globals(), 'Draft')
-Part = LazyLoader('Part', globals(), 'Part')
-
-if FreeCAD.GuiUp:
- import FreeCADGui
-
-__title__ = "Path Waterline Operation"
-__author__ = "russ4262 (Russell Johnson), sliptonic (Brad Collette)"
-__url__ = "http://www.freecadweb.org"
-__doc__ = "Class and implementation of Mill Facing operation."
-
-PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
-# PathLog.trackModule(PathLog.thisModule())
-
-
-# Qt translation handling
-def translate(context, text, disambig=None):
- return QtCore.QCoreApplication.translate(context, text, disambig)
-
-
-# OCL must be installed
-try:
- import ocl
-except ImportError:
- FreeCAD.Console.PrintError(
- translate("Path_Waterline", "This operation requires OpenCamLib to be installed.") + "\n")
- import sys
- sys.exit(translate("Path_Waterline", "This operation requires OpenCamLib to be installed."))
-
-
-class ObjectWaterline(PathOp.ObjectOp):
- '''Proxy object for Surfacing operation.'''
-
- def baseObject(self):
- '''baseObject() ... returns super of receiver
- Used to call base implementation in overwritten functions.'''
- return super(self.__class__, self)
-
- def opFeatures(self, obj):
- '''opFeatures(obj) ... return all standard features and edges based geomtries'''
- return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureHeights | PathOp.FeatureStepDown | PathOp.FeatureCoolant | PathOp.FeatureBaseFaces
-
- def initOperation(self, obj):
- '''initPocketOp(obj) ...
- Initialize the operation - property creation and property editor status.'''
- self.initOpProperties(obj)
-
- # For debugging
- if PathLog.getLevel(PathLog.thisModule()) != 4:
- obj.setEditorMode('ShowTempObjects', 2) # hide
-
- if not hasattr(obj, 'DoNotSetDefaultValues'):
- self.setEditorProperties(obj)
-
- def initOpProperties(self, obj):
- '''initOpProperties(obj) ... create operation specific properties'''
- PROPS = [
- ("App::PropertyBool", "ShowTempObjects", "Debug",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Show the temporary path construction objects when module is in DEBUG mode.")),
-
- ("App::PropertyDistance", "AngularDeflection", "Mesh Conversion",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate the mesh. Smaller values increase processing time a lot.")),
- ("App::PropertyDistance", "LinearDeflection", "Mesh Conversion",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate the mesh. Smaller values do not increase processing time much.")),
-
- ("App::PropertyInteger", "AvoidLastX_Faces", "Selected Geometry Settings",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Avoid cutting the last 'N' faces in the Base Geometry list of selected faces.")),
- ("App::PropertyBool", "AvoidLastX_InternalFeatures", "Selected Geometry Settings",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Do not cut internal features on avoided faces.")),
- ("App::PropertyDistance", "BoundaryAdjustment", "Selected Geometry Settings",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Positive values push the cutter toward, or beyond, the boundary. Negative values retract the cutter away from the boundary.")),
- ("App::PropertyBool", "BoundaryEnforcement", "Selected Geometry Settings",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "If true, the cutter will remain inside the boundaries of the model or selected face(s).")),
- ("App::PropertyEnumeration", "HandleMultipleFeatures", "Selected Geometry Settings",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose how to process multiple Base Geometry features.")),
- ("App::PropertyDistance", "InternalFeaturesAdjustment", "Selected Geometry Settings",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Positive values push the cutter toward, or into, the feature. Negative values retract the cutter away from the feature.")),
- ("App::PropertyBool", "InternalFeaturesCut", "Selected Geometry Settings",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Ignore internal feature areas within a larger selected face.")),
-
- ("App::PropertyEnumeration", "Algorithm", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Select the algorithm to use: OCL Dropcutter*, or Experimental.")),
- ("App::PropertyEnumeration", "BoundBox", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Select the overall boundary for the operation. ")),
- ("App::PropertyVectorDistance", "CircularCenterCustom", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the start point for circular cut patterns.")),
- ("App::PropertyEnumeration", "CircularCenterAt", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose location of the center point for starting the circular pattern.")),
- ("App::PropertyEnumeration", "ClearLastLayer", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Set to clear last layer in a `Multi-pass` operation.")),
- ("App::PropertyEnumeration", "CutMode", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the direction for the cutting tool to engage the material: Climb (ClockWise) or Conventional (CounterClockWise)")),
- ("App::PropertyEnumeration", "CutPattern", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the geometric clearing pattern to use for the operation.")),
- ("App::PropertyFloat", "CutPatternAngle", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "The yaw angle used for certain clearing patterns")),
- ("App::PropertyBool", "CutPatternReversed", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Reverse the cut order of the stepover paths. For circular cut patterns, begin at the outside and work toward the center.")),
- ("App::PropertyDistance", "DepthOffset", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the Z-axis depth offset from the target surface.")),
- ("App::PropertyEnumeration", "LayerMode", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Complete the operation in a single pass at depth, or mulitiple passes to final depth.")),
- ("App::PropertyEnumeration", "ProfileEdges", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Profile the edges of the selection.")),
- ("App::PropertyDistance", "SampleInterval", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the sampling resolution. Smaller values quickly increase processing time.")),
- ("App::PropertyPercent", "StepOver", "Clearing Options",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the stepover percentage, based on the tool's diameter.")),
-
- ("App::PropertyBool", "OptimizeLinearPaths", "Optimization",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable optimization of linear paths (co-linear points). Removes unnecessary co-linear points from G-Code output.")),
- ("App::PropertyBool", "OptimizeStepOverTransitions", "Optimization",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable separate optimization of transitions between, and breaks within, each step over path.")),
- ("App::PropertyDistance", "GapThreshold", "Optimization",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Collinear and co-radial artifact gaps that are smaller than this threshold are closed in the path.")),
- ("App::PropertyString", "GapSizes", "Optimization",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Feedback: three smallest gaps identified in the path geometry.")),
-
- ("App::PropertyVectorDistance", "StartPoint", "Start Point",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "The custom start point for the path of this operation")),
- ("App::PropertyBool", "UseStartPoint", "Start Point",
- QtCore.QT_TRANSLATE_NOOP("App::Property", "Make True, if specifying a Start Point"))
- ]
-
- missing = list()
- for (prtyp, nm, grp, tt) in PROPS:
- if not hasattr(obj, nm):
- obj.addProperty(prtyp, nm, grp, tt)
- missing.append(nm)
-
- # Set enumeration lists for enumeration properties
- if len(missing) > 0:
- ENUMS = self._propertyEnumerations()
- for n in ENUMS:
- if n in missing:
- cmdStr = 'obj.{}={}'.format(n, ENUMS[n])
- exec(cmdStr)
-
- self.addedAllProperties = True
-
- def _propertyEnumerations(self):
- # Enumeration lists for App::PropertyEnumeration properties
- return {
- 'Algorithm': ['OCL Dropcutter', 'Experimental'],
- 'BoundBox': ['BaseBoundBox', 'Stock'],
- 'CircularCenterAt': ['CenterOfMass', 'CenterOfBoundBox', 'XminYmin', 'Custom'],
- 'ClearLastLayer': ['Off', 'Line', 'Circular', 'CircularZigZag', 'Offset', 'ZigZag'],
- 'CutMode': ['Conventional', 'Climb'],
- 'CutPattern': ['None', 'Line', 'Circular', 'CircularZigZag', 'Offset', 'ZigZag'], # Additional goals ['Offset', 'Spiral', 'ZigZagOffset', 'Grid', 'Triangle']
- 'HandleMultipleFeatures': ['Collectively', 'Individually'],
- 'LayerMode': ['Single-pass', 'Multi-pass'],
- 'ProfileEdges': ['None', 'Only', 'First', 'Last'],
- }
-
- def setEditorProperties(self, obj):
- # Used to hide inputs in properties list
- show = 0
- hide = 2
- cpShow = 0
- expMode = 0
- obj.setEditorMode('BoundaryEnforcement', hide)
- obj.setEditorMode('ProfileEdges', hide)
- obj.setEditorMode('InternalFeaturesAdjustment', hide)
- obj.setEditorMode('InternalFeaturesCut', hide)
- obj.setEditorMode('GapSizes', hide)
- obj.setEditorMode('GapThreshold', hide)
- obj.setEditorMode('AvoidLastX_Faces', hide)
- obj.setEditorMode('AvoidLastX_InternalFeatures', hide)
- obj.setEditorMode('BoundaryAdjustment', hide)
- obj.setEditorMode('HandleMultipleFeatures', hide)
- if hasattr(obj, 'EnableRotation'):
- obj.setEditorMode('EnableRotation', hide)
- if obj.CutPattern == 'None':
- show = 2
- hide = 2
- cpShow = 2
- # elif obj.CutPattern in ['Line', 'ZigZag']:
- # show = 0
- # hide = 2
- elif obj.CutPattern in ['Circular', 'CircularZigZag']:
- show = 2 # hide
- hide = 0 # show
- # obj.setEditorMode('StepOver', cpShow)
- obj.setEditorMode('CutPatternAngle', show)
- obj.setEditorMode('CircularCenterAt', hide)
- obj.setEditorMode('CircularCenterCustom', hide)
- if obj.Algorithm == 'Experimental':
- expMode = 2
- obj.setEditorMode('SampleInterval', expMode)
- obj.setEditorMode('LinearDeflection', expMode)
- obj.setEditorMode('AngularDeflection', expMode)
-
- def onChanged(self, obj, prop):
- if hasattr(self, 'addedAllProperties'):
- if self.addedAllProperties is True:
- if prop in ['Algorithm', 'CutPattern']:
- self.setEditorProperties(obj)
-
- def opOnDocumentRestored(self, obj):
- self.initOpProperties(obj)
-
- if PathLog.getLevel(PathLog.thisModule()) != 4:
- obj.setEditorMode('ShowTempObjects', 2) # hide
- else:
- obj.setEditorMode('ShowTempObjects', 0) # show
-
- self.setEditorProperties(obj)
-
- def opSetDefaultValues(self, obj, job):
- '''opSetDefaultValues(obj, job) ... initialize defaults'''
- job = PathUtils.findParentJob(obj)
-
- obj.OptimizeLinearPaths = True
- obj.InternalFeaturesCut = True
- obj.OptimizeStepOverTransitions = False
- obj.BoundaryEnforcement = True
- obj.UseStartPoint = False
- obj.AvoidLastX_InternalFeatures = True
- obj.CutPatternReversed = False
- obj.StartPoint.x = 0.0
- obj.StartPoint.y = 0.0
- obj.StartPoint.z = obj.ClearanceHeight.Value
- obj.Algorithm = 'OCL Dropcutter'
- obj.ProfileEdges = 'None'
- obj.LayerMode = 'Single-pass'
- obj.CutMode = 'Conventional'
- obj.CutPattern = 'None'
- obj.HandleMultipleFeatures = 'Collectively' # 'Individually'
- obj.CircularCenterAt = 'CenterOfMass' # 'CenterOfBoundBox', 'XminYmin', 'Custom'
- obj.GapSizes = 'No gaps identified.'
- obj.ClearLastLayer = 'Off'
- obj.StepOver = 100
- obj.CutPatternAngle = 0.0
- obj.DepthOffset.Value = 0.0
- obj.SampleInterval.Value = 1.0
- obj.BoundaryAdjustment.Value = 0.0
- obj.InternalFeaturesAdjustment.Value = 0.0
- obj.AvoidLastX_Faces = 0
- obj.CircularCenterCustom.x = 0.0
- obj.CircularCenterCustom.y = 0.0
- obj.CircularCenterCustom.z = 0.0
- obj.GapThreshold.Value = 0.005
- obj.LinearDeflection.Value = 0.0001
- obj.AngularDeflection.Value = 0.25
- # For debugging
- obj.ShowTempObjects = False
-
- # need to overwrite the default depth calculations for facing
- d = None
- if job:
- if job.Stock:
- d = PathUtils.guessDepths(job.Stock.Shape, None)
- PathLog.debug("job.Stock exists")
- else:
- PathLog.debug("job.Stock NOT exist")
- else:
- PathLog.debug("job NOT exist")
-
- if d is not None:
- obj.OpFinalDepth.Value = d.final_depth
- obj.OpStartDepth.Value = d.start_depth
- else:
- obj.OpFinalDepth.Value = -10
- obj.OpStartDepth.Value = 10
-
- PathLog.debug('Default OpFinalDepth: {}'.format(obj.OpFinalDepth.Value))
- PathLog.debug('Defualt OpStartDepth: {}'.format(obj.OpStartDepth.Value))
-
- def opApplyPropertyLimits(self, obj):
- '''opApplyPropertyLimits(obj) ... Apply necessary limits to user input property values before performing main operation.'''
- # Limit sample interval
- if obj.SampleInterval.Value < 0.001:
- obj.SampleInterval.Value = 0.001
- PathLog.error(translate('PathWaterline', 'Sample interval limits are 0.001 to 25.4 millimeters.'))
- if obj.SampleInterval.Value > 25.4:
- obj.SampleInterval.Value = 25.4
- PathLog.error(translate('PathWaterline', 'Sample interval limits are 0.001 to 25.4 millimeters.'))
-
- # Limit cut pattern angle
- if obj.CutPatternAngle < -360.0:
- obj.CutPatternAngle = 0.0
- PathLog.error(translate('PathWaterline', 'Cut pattern angle limits are +-360 degrees.'))
- if obj.CutPatternAngle >= 360.0:
- obj.CutPatternAngle = 0.0
- PathLog.error(translate('PathWaterline', 'Cut pattern angle limits are +- 360 degrees.'))
-
- # Limit StepOver to natural number percentage
- if obj.StepOver > 100:
- obj.StepOver = 100
- if obj.StepOver < 1:
- obj.StepOver = 1
-
- # Limit AvoidLastX_Faces to zero and positive values
- if obj.AvoidLastX_Faces < 0:
- obj.AvoidLastX_Faces = 0
- PathLog.error(translate('PathWaterline', 'AvoidLastX_Faces: Only zero or positive values permitted.'))
- if obj.AvoidLastX_Faces > 100:
- obj.AvoidLastX_Faces = 100
- PathLog.error(translate('PathWaterline', 'AvoidLastX_Faces: Avoid last X faces count limited to 100.'))
-
- def opExecute(self, obj):
- '''opExecute(obj) ... process surface operation'''
- PathLog.track()
-
- self.modelSTLs = list()
- self.safeSTLs = list()
- self.modelTypes = list()
- self.boundBoxes = list()
- self.profileShapes = list()
- self.collectiveShapes = list()
- self.individualShapes = list()
- self.avoidShapes = list()
- self.geoTlrnc = None
- self.tempGroup = None
- self.CutClimb = False
- self.closedGap = False
- self.gaps = [0.1, 0.2, 0.3]
- CMDS = list()
- modelVisibility = list()
- FCAD = FreeCAD.ActiveDocument
-
- # Set debugging behavior
- self.showDebugObjects = False # Set to true if you want a visual DocObjects created for some path construction objects
- self.showDebugObjects = obj.ShowTempObjects
- deleteTempsFlag = True # Set to False for debugging
- if PathLog.getLevel(PathLog.thisModule()) == 4:
- deleteTempsFlag = False
- else:
- self.showDebugObjects = False
-
- # mark beginning of operation and identify parent Job
- PathLog.info('\nBegin Waterline operation...')
- startTime = time.time()
-
- # Identify parent Job
- JOB = PathUtils.findParentJob(obj)
- if JOB is None:
- PathLog.error(translate('PathWaterline', "No JOB"))
- return
- self.stockZMin = JOB.Stock.Shape.BoundBox.ZMin
-
- # set cut mode; reverse as needed
- if obj.CutMode == 'Climb':
- self.CutClimb = True
- if obj.CutPatternReversed is True:
- if self.CutClimb is True:
- self.CutClimb = False
- else:
- self.CutClimb = True
-
- # Begin GCode for operation with basic information
- # ... and move cutter to clearance height and startpoint
- output = ''
- if obj.Comment != '':
- self.commandlist.append(Path.Command('N ({})'.format(str(obj.Comment)), {}))
- self.commandlist.append(Path.Command('N ({})'.format(obj.Label), {}))
- self.commandlist.append(Path.Command('N (Tool type: {})'.format(str(obj.ToolController.Tool.ToolType)), {}))
- self.commandlist.append(Path.Command('N (Compensated Tool Path. Diameter: {})'.format(str(obj.ToolController.Tool.Diameter)), {}))
- self.commandlist.append(Path.Command('N (Sample interval: {})'.format(str(obj.SampleInterval.Value)), {}))
- self.commandlist.append(Path.Command('N (Step over %: {})'.format(str(obj.StepOver)), {}))
- self.commandlist.append(Path.Command('N ({})'.format(output), {}))
- self.commandlist.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid}))
- if obj.UseStartPoint is True:
- self.commandlist.append(Path.Command('G0', {'X': obj.StartPoint.x, 'Y': obj.StartPoint.y, 'F': self.horizRapid}))
-
- # Instantiate additional class operation variables
- self.resetOpVariables()
-
- # Impose property limits
- self.opApplyPropertyLimits(obj)
-
- # Create temporary group for temporary objects, removing existing
- # if self.showDebugObjects is True:
- tempGroupName = 'tempPathWaterlineGroup'
- if FCAD.getObject(tempGroupName):
- for to in FCAD.getObject(tempGroupName).Group:
- FCAD.removeObject(to.Name)
- FCAD.removeObject(tempGroupName) # remove temp directory if already exists
- if FCAD.getObject(tempGroupName + '001'):
- for to in FCAD.getObject(tempGroupName + '001').Group:
- FCAD.removeObject(to.Name)
- FCAD.removeObject(tempGroupName + '001') # remove temp directory if already exists
- tempGroup = FCAD.addObject('App::DocumentObjectGroup', tempGroupName)
- tempGroupName = tempGroup.Name
- self.tempGroup = tempGroup
- tempGroup.purgeTouched()
- # Add temp object to temp group folder with following code:
- # ... self.tempGroup.addObject(OBJ)
-
- # Setup cutter for OCL and cutout value for operation - based on tool controller properties
- self.cutter = self.setOclCutter(obj)
- self.safeCutter = self.setOclCutter(obj, safe=True)
- if self.cutter is False or self.safeCutter is False:
- PathLog.error(translate('PathWaterline', "Canceling Waterline operation. Error creating OCL cutter."))
- return
- toolDiam = self.cutter.getDiameter()
- self.cutOut = (toolDiam * (float(obj.StepOver) / 100.0))
- self.radius = toolDiam / 2.0
- self.gaps = [toolDiam, toolDiam, toolDiam]
-
- # Get height offset values for later use
- self.SafeHeightOffset = JOB.SetupSheet.SafeHeightOffset.Value
- self.ClearHeightOffset = JOB.SetupSheet.ClearanceHeightOffset.Value
-
- # Set deflection values for mesh generation
- useDGT = False
- try: # try/except is for Path Jobs created before GeometryTolerance
- self.geoTlrnc = JOB.GeometryTolerance.Value
- if self.geoTlrnc == 0.0:
- useDGT = True
- except AttributeError as ee:
- PathLog.warning('{}\nPlease set Job.GeometryTolerance to an acceptable value. Using PathPreferences.defaultGeometryTolerance().'.format(ee))
- useDGT = True
- if useDGT:
- import PathScripts.PathPreferences as PathPreferences
- self.geoTlrnc = PathPreferences.defaultGeometryTolerance()
-
- # Calculate default depthparams for operation
- self.depthParams = PathUtils.depth_params(obj.ClearanceHeight.Value, obj.SafeHeight.Value, obj.StartDepth.Value, obj.StepDown.Value, 0.0, obj.FinalDepth.Value)
- self.midDep = (obj.StartDepth.Value + obj.FinalDepth.Value) / 2.0
-
- # make circle for workplane
- self.wpc = Part.makeCircle(2.0)
-
- # Save model visibilities for restoration
- if FreeCAD.GuiUp:
- for m in range(0, len(JOB.Model.Group)):
- mNm = JOB.Model.Group[m].Name
- modelVisibility.append(FreeCADGui.ActiveDocument.getObject(mNm).Visibility)
-
- # Setup STL, model type, and bound box containers for each model in Job
- for m in range(0, len(JOB.Model.Group)):
- M = JOB.Model.Group[m]
- self.modelSTLs.append(False)
- self.safeSTLs.append(False)
- self.profileShapes.append(False)
- # Set bound box
- if obj.BoundBox == 'BaseBoundBox':
- if M.TypeId.startswith('Mesh'):
- self.modelTypes.append('M') # Mesh
- self.boundBoxes.append(M.Mesh.BoundBox)
- else:
- self.modelTypes.append('S') # Solid
- self.boundBoxes.append(M.Shape.BoundBox)
- elif obj.BoundBox == 'Stock':
- self.modelTypes.append('S') # Solid
- self.boundBoxes.append(JOB.Stock.Shape.BoundBox)
-
- # ###### MAIN COMMANDS FOR OPERATION ######
-
- # Begin processing obj.Base data and creating GCode
- # Process selected faces, if available
- pPM = self._preProcessModel(JOB, obj)
- if pPM is False:
- PathLog.error('Unable to pre-process obj.Base.')
- else:
- (FACES, VOIDS) = pPM
-
- # Create OCL.stl model objects
- if obj.Algorithm == 'OCL Dropcutter':
- self._prepareModelSTLs(JOB, obj)
- PathLog.debug('obj.LinearDeflection.Value: {}'.format(obj.LinearDeflection.Value))
- PathLog.debug('obj.AngularDeflection.Value: {}'.format(obj.AngularDeflection.Value))
-
- for m in range(0, len(JOB.Model.Group)):
- Mdl = JOB.Model.Group[m]
- if FACES[m] is False:
- PathLog.error('No data for model base: {}'.format(JOB.Model.Group[m].Label))
- else:
- if m > 0:
- # Raise to clearance between models
- CMDS.append(Path.Command('N (Transition to base: {}.)'.format(Mdl.Label)))
- CMDS.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid}))
- PathLog.info('Working on Model.Group[{}]: {}'.format(m, Mdl.Label))
- # make stock-model-voidShapes STL model for avoidance detection on transitions
- if obj.Algorithm == 'OCL Dropcutter':
- self._makeSafeSTL(JOB, obj, m, FACES[m], VOIDS[m])
- # time.sleep(0.2)
- # Process model/faces - OCL objects must be ready
- CMDS.extend(self._processCutAreas(JOB, obj, m, FACES[m], VOIDS[m]))
-
- # Save gcode produced
- self.commandlist.extend(CMDS)
-
- # ###### CLOSING COMMANDS FOR OPERATION ######
-
- # Delete temporary objects
- # Restore model visibilities for restoration
- if FreeCAD.GuiUp:
- FreeCADGui.ActiveDocument.getObject(tempGroupName).Visibility = False
- for m in range(0, len(JOB.Model.Group)):
- M = JOB.Model.Group[m]
- M.Visibility = modelVisibility[m]
-
- if deleteTempsFlag is True:
- for to in tempGroup.Group:
- if hasattr(to, 'Group'):
- for go in to.Group:
- FCAD.removeObject(go.Name)
- FCAD.removeObject(to.Name)
- FCAD.removeObject(tempGroupName)
- else:
- if len(tempGroup.Group) == 0:
- FCAD.removeObject(tempGroupName)
- else:
- tempGroup.purgeTouched()
-
- # Provide user feedback for gap sizes
- gaps = list()
- for g in self.gaps:
- if g != toolDiam:
- gaps.append(g)
- if len(gaps) > 0:
- obj.GapSizes = '{} mm'.format(gaps)
- else:
- if self.closedGap is True:
- obj.GapSizes = 'Closed gaps < Gap Threshold.'
- else:
- obj.GapSizes = 'No gaps identified.'
-
- # clean up class variables
- self.resetOpVariables()
- self.deleteOpVariables()
-
- self.modelSTLs = None
- self.safeSTLs = None
- self.modelTypes = None
- self.boundBoxes = None
- self.gaps = None
- self.closedGap = None
- self.SafeHeightOffset = None
- self.ClearHeightOffset = None
- self.depthParams = None
- self.midDep = None
- self.wpc = None
- del self.modelSTLs
- del self.safeSTLs
- del self.modelTypes
- del self.boundBoxes
- del self.gaps
- del self.closedGap
- del self.SafeHeightOffset
- del self.ClearHeightOffset
- del self.depthParams
- del self.midDep
- del self.wpc
-
- execTime = time.time() - startTime
- PathLog.info('Operation time: {} sec.'.format(execTime))
-
- return True
-
- # Methods for constructing the cut area
- def _preProcessModel(self, JOB, obj):
- PathLog.debug('_preProcessModel()')
-
- FACES = list()
- VOIDS = list()
- fShapes = list()
- vShapes = list()
- preProcEr = translate('PathWaterline', 'Error pre-processing Face')
- warnFinDep = translate('PathWaterline', 'Final Depth might need to be lower. Internal features detected in Face')
- GRP = JOB.Model.Group
- lenGRP = len(GRP)
-
- # Crete place holders for each base model in Job
- for m in range(0, lenGRP):
- FACES.append(False)
- VOIDS.append(False)
- fShapes.append(False)
- vShapes.append(False)
-
- # The user has selected subobjects from the base. Pre-Process each.
- if obj.Base and len(obj.Base) > 0:
- PathLog.debug(' -obj.Base exists. Pre-processing for selected faces.')
-
- (FACES, VOIDS) = self._identifyFacesAndVoids(JOB, obj, FACES, VOIDS)
-
- # Cycle through each base model, processing faces for each
- for m in range(0, lenGRP):
- base = GRP[m]
- (mFS, mVS, mPS) = self._preProcessFacesAndVoids(obj, base, m, FACES, VOIDS)
- fShapes[m] = mFS
- vShapes[m] = mVS
- self.profileShapes[m] = mPS
- else:
- PathLog.debug(' -No obj.Base data.')
- for m in range(0, lenGRP):
- self.modelSTLs[m] = True
-
- # Process each model base, as a whole, as needed
- # PathLog.debug(' -Pre-processing all models in Job.')
- for m in range(0, lenGRP):
- if fShapes[m] is False:
- PathLog.debug(' -Pre-processing {} as a whole.'.format(GRP[m].Label))
- if obj.BoundBox == 'BaseBoundBox':
- base = GRP[m]
- elif obj.BoundBox == 'Stock':
- base = JOB.Stock
-
- pPEB = self._preProcessEntireBase(obj, base, m)
- if pPEB is False:
- PathLog.error(' -Failed to pre-process base as a whole.')
- else:
- (fcShp, prflShp) = pPEB
- if fcShp is not False:
- if fcShp is True:
- PathLog.debug(' -fcShp is True.')
- fShapes[m] = True
- else:
- fShapes[m] = [fcShp]
- if prflShp is not False:
- if fcShp is not False:
- PathLog.debug('vShapes[{}]: {}'.format(m, vShapes[m]))
- if vShapes[m] is not False:
- PathLog.debug(' -Cutting void from base profile shape.')
- adjPS = prflShp.cut(vShapes[m][0])
- self.profileShapes[m] = [adjPS]
- else:
- PathLog.debug(' -vShapes[m] is False.')
- self.profileShapes[m] = [prflShp]
- else:
- PathLog.debug(' -Saving base profile shape.')
- self.profileShapes[m] = [prflShp]
- PathLog.debug('self.profileShapes[{}]: {}'.format(m, self.profileShapes[m]))
- # Efor
-
- return (fShapes, vShapes)
-
- def _identifyFacesAndVoids(self, JOB, obj, F, V):
- TUPS = list()
- GRP = JOB.Model.Group
- lenGRP = len(GRP)
-
- # Separate selected faces into (base, face) tuples and flag model(s) for STL creation
- for (bs, SBS) in obj.Base:
- for sb in SBS:
- # Flag model for STL creation
- mdlIdx = None
- for m in range(0, lenGRP):
- if bs is GRP[m]:
- self.modelSTLs[m] = True
- mdlIdx = m
- break
- TUPS.append((mdlIdx, bs, sb)) # (model idx, base, sub)
-
- # Apply `AvoidXFaces` value
- faceCnt = len(TUPS)
- add = faceCnt - obj.AvoidLastX_Faces
- for bst in range(0, faceCnt):
- (m, base, sub) = TUPS[bst]
- shape = getattr(base.Shape, sub)
- if isinstance(shape, Part.Face):
- faceIdx = int(sub[4:]) - 1
- if bst < add:
- if F[m] is False:
- F[m] = list()
- F[m].append((shape, faceIdx))
- else:
- if V[m] is False:
- V[m] = list()
- V[m].append((shape, faceIdx))
- return (F, V)
-
- def _preProcessFacesAndVoids(self, obj, base, m, FACES, VOIDS):
- mFS = False
- mVS = False
- mPS = False
- mIFS = list()
- BB = base.Shape.BoundBox
-
- if FACES[m] is not False:
- isHole = False
- if obj.HandleMultipleFeatures == 'Collectively':
- cont = True
- fsL = list() # face shape list
- ifL = list() # avoid shape list
- outFCS = list()
-
- # Get collective envelope slice of selected faces
- for (fcshp, fcIdx) in FACES[m]:
- fNum = fcIdx + 1
- fsL.append(fcshp)
- gFW = self._getFaceWires(base, fcshp, fcIdx)
- if gFW is False:
- PathLog.debug('Failed to get wires from Face{}'.format(fNum))
- elif gFW[0] is False:
- PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum))
- else:
- ((otrFace, raised), intWires) = gFW
- outFCS.append(otrFace)
- if obj.InternalFeaturesCut is False:
- if intWires is not False:
- for (iFace, rsd) in intWires:
- ifL.append(iFace)
-
- PathLog.debug('Attempting to get cross-section of collective faces.')
- if len(outFCS) == 0:
- PathLog.error('Cannot process selected faces. Check horizontal surface exposure.'.format(fNum))
- cont = False
- else:
- cfsL = Part.makeCompound(outFCS)
-
- # Handle profile edges request
- if cont is True and obj.ProfileEdges != 'None':
- ofstVal = self._calculateOffsetValue(obj, isHole)
- psOfst = self._extractFaceOffset(obj, cfsL, ofstVal)
- if psOfst is not False:
- mPS = [psOfst]
- if obj.ProfileEdges == 'Only':
- mFS = True
- cont = False
- else:
- PathLog.error(' -Failed to create profile geometry for selected faces.')
- cont = False
-
- if cont is True:
- if self.showDebugObjects is True:
- T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCollectiveShape')
- T.Shape = cfsL
- T.purgeTouched()
- self.tempGroup.addObject(T)
-
- ofstVal = self._calculateOffsetValue(obj, isHole)
- faceOfstShp = self._extractFaceOffset(obj, cfsL, ofstVal)
- if faceOfstShp is False:
- PathLog.error(' -Failed to create offset face.')
- cont = False
-
- if cont is True:
- lenIfL = len(ifL)
- if obj.InternalFeaturesCut is False:
- if lenIfL == 0:
- PathLog.debug(' -No internal features saved.')
- else:
- if lenIfL == 1:
- casL = ifL[0]
- else:
- casL = Part.makeCompound(ifL)
- if self.showDebugObjects is True:
- C = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCompoundIntFeat')
- C.Shape = casL
- C.purgeTouched()
- self.tempGroup.addObject(C)
- ofstVal = self._calculateOffsetValue(obj, isHole=True)
- intOfstShp = self._extractFaceOffset(obj, casL, ofstVal)
- mIFS.append(intOfstShp)
- # faceOfstShp = faceOfstShp.cut(intOfstShp)
-
- mFS = [faceOfstShp]
- # Eif
-
- elif obj.HandleMultipleFeatures == 'Individually':
- for (fcshp, fcIdx) in FACES[m]:
- cont = True
- fsL = list() # face shape list
- ifL = list() # avoid shape list
- fNum = fcIdx + 1
- outerFace = False
-
- gFW = self._getFaceWires(base, fcshp, fcIdx)
- if gFW is False:
- PathLog.debug('Failed to get wires from Face{}'.format(fNum))
- cont = False
- elif gFW[0] is False:
- PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum))
- cont = False
- outerFace = False
- else:
- ((otrFace, raised), intWires) = gFW
- outerFace = otrFace
- if obj.InternalFeaturesCut is False:
- if intWires is not False:
- for (iFace, rsd) in intWires:
- ifL.append(iFace)
-
- if outerFace is not False:
- PathLog.debug('Attempting to create offset face of Face{}'.format(fNum))
-
- if obj.ProfileEdges != 'None':
- ofstVal = self._calculateOffsetValue(obj, isHole)
- psOfst = self._extractFaceOffset(obj, outerFace, ofstVal)
- if psOfst is not False:
- if mPS is False:
- mPS = list()
- mPS.append(psOfst)
- if obj.ProfileEdges == 'Only':
- if mFS is False:
- mFS = list()
- mFS.append(True)
- cont = False
- else:
- PathLog.error(' -Failed to create profile geometry for Face{}.'.format(fNum))
- cont = False
-
- if cont is True:
- ofstVal = self._calculateOffsetValue(obj, isHole)
- faceOfstShp = self._extractFaceOffset(obj, slc, ofstVal)
-
- lenIfl = len(ifL)
- if obj.InternalFeaturesCut is False and lenIfl > 0:
- if lenIfl == 1:
- casL = ifL[0]
- else:
- casL = Part.makeCompound(ifL)
-
- ofstVal = self._calculateOffsetValue(obj, isHole=True)
- intOfstShp = self._extractFaceOffset(obj, casL, ofstVal)
- mIFS.append(intOfstShp)
- # faceOfstShp = faceOfstShp.cut(intOfstShp)
-
- if mFS is False:
- mFS = list()
- mFS.append(faceOfstShp)
- # Eif
- # Efor
- # Eif
- # Eif
-
- if len(mIFS) > 0:
- if mVS is False:
- mVS = list()
- for ifs in mIFS:
- mVS.append(ifs)
-
- if VOIDS[m] is not False:
- PathLog.debug('Processing avoid faces.')
- cont = True
- isHole = False
- outFCS = list()
- intFEAT = list()
-
- for (fcshp, fcIdx) in VOIDS[m]:
- fNum = fcIdx + 1
- gFW = self._getFaceWires(base, fcshp, fcIdx)
- if gFW is False:
- PathLog.debug('Failed to get wires from avoid Face{}'.format(fNum))
- cont = False
- else:
- ((otrFace, raised), intWires) = gFW
- outFCS.append(otrFace)
- if obj.AvoidLastX_InternalFeatures is False:
- if intWires is not False:
- for (iFace, rsd) in intWires:
- intFEAT.append(iFace)
-
- lenOtFcs = len(outFCS)
- if lenOtFcs == 0:
- cont = False
- else:
- if lenOtFcs == 1:
- avoid = outFCS[0]
- else:
- avoid = Part.makeCompound(outFCS)
-
- if self.showDebugObjects is True:
- PathLog.debug('*** tmpAvoidArea')
- P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidEnvelope')
- P.Shape = avoid
- # P.recompute()
- P.purgeTouched()
- self.tempGroup.addObject(P)
-
- if cont is True:
- if self.showDebugObjects is True:
- PathLog.debug('*** tmpVoidCompound')
- P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidCompound')
- P.Shape = avoid
- # P.recompute()
- P.purgeTouched()
- self.tempGroup.addObject(P)
- ofstVal = self._calculateOffsetValue(obj, isHole, isVoid=True)
- avdOfstShp = self._extractFaceOffset(obj, avoid, ofstVal)
- if avdOfstShp is False:
- PathLog.error('Failed to create collective offset avoid face.')
- cont = False
-
- if cont is True:
- avdShp = avdOfstShp
-
- if obj.AvoidLastX_InternalFeatures is False and len(intFEAT) > 0:
- if len(intFEAT) > 1:
- ifc = Part.makeCompound(intFEAT)
- else:
- ifc = intFEAT[0]
- ofstVal = self._calculateOffsetValue(obj, isHole=True)
- ifOfstShp = self._extractFaceOffset(obj, ifc, ofstVal)
- if ifOfstShp is False:
- PathLog.error('Failed to create collective offset avoid internal features.')
- else:
- avdShp = avdOfstShp.cut(ifOfstShp)
-
- if mVS is False:
- mVS = list()
- mVS.append(avdShp)
-
-
- return (mFS, mVS, mPS)
-
- def _getFaceWires(self, base, fcshp, fcIdx):
- outFace = False
- INTFCS = list()
- fNum = fcIdx + 1
- # preProcEr = translate('PathWaterline', 'Error pre-processing Face')
- warnFinDep = translate('PathWaterline', 'Final Depth might need to be lower. Internal features detected in Face')
-
- PathLog.debug('_getFaceWires() from Face{}'.format(fNum))
- WIRES = self._extractWiresFromFace(base, fcshp)
- if WIRES is False:
- PathLog.error('Failed to extract wires from Face{}'.format(fNum))
- return False
-
- # Process remaining internal features, adding to FCS list
- lenW = len(WIRES)
- for w in range(0, lenW):
- (wire, rsd) = WIRES[w]
- PathLog.debug('Processing Wire{} in Face{}. isRaised: {}'.format(w + 1, fNum, rsd))
- if wire.isClosed() is False:
- PathLog.debug(' -wire is not closed.')
- else:
- slc = self._flattenWireToFace(wire)
- if slc is False:
- PathLog.error('FAILED to identify horizontal exposure on Face{}.'.format(fNum))
- else:
- if w == 0:
- outFace = (slc, rsd)
- else:
- # add to VOIDS so cutter avoids area.
- PathLog.warning(warnFinDep + str(fNum) + '.')
- INTFCS.append((slc, rsd))
- if len(INTFCS) == 0:
- return (outFace, False)
- else:
- return (outFace, INTFCS)
-
- def _preProcessEntireBase(self, obj, base, m):
- cont = True
- isHole = False
- prflShp = False
- # Create envelope, extract cross-section and make offset co-planar shape
- # baseEnv = PathUtils.getEnvelope(base.Shape, subshape=None, depthparams=self.depthParams)
-
- try:
- baseEnv = PathUtils.getEnvelope(partshape=base.Shape, subshape=None, depthparams=self.depthParams) # Produces .Shape
- except Exception as ee:
- PathLog.error(str(ee))
- shell = base.Shape.Shells[0]
- solid = Part.makeSolid(shell)
- try:
- baseEnv = PathUtils.getEnvelope(partshape=solid, subshape=None, depthparams=self.depthParams) # Produces .Shape
- except Exception as eee:
- PathLog.error(str(eee))
- cont = False
- # time.sleep(0.2)
-
- if cont is True:
- csFaceShape = self._getShapeSlice(baseEnv)
- if csFaceShape is False:
- PathLog.debug('_getShapeSlice(baseEnv) failed')
- csFaceShape = self._getCrossSection(baseEnv)
- if csFaceShape is False:
- PathLog.debug('_getCrossSection(baseEnv) failed')
- csFaceShape = self._getSliceFromEnvelope(baseEnv)
- if csFaceShape is False:
- PathLog.error('Failed to slice baseEnv shape.')
- cont = False
-
- if cont is True and obj.ProfileEdges != 'None':
- PathLog.debug(' -Attempting profile geometry for model base.')
- ofstVal = self._calculateOffsetValue(obj, isHole)
- psOfst = self._extractFaceOffset(obj, csFaceShape, ofstVal)
- if psOfst is not False:
- if obj.ProfileEdges == 'Only':
- return (True, psOfst)
- prflShp = psOfst
- else:
- PathLog.error(' -Failed to create profile geometry.')
- cont = False
-
- if cont is True:
- ofstVal = self._calculateOffsetValue(obj, isHole)
- faceOffsetShape = self._extractFaceOffset(obj, csFaceShape, ofstVal)
- if faceOffsetShape is False:
- PathLog.error('_extractFaceOffset() failed.')
- else:
- faceOffsetShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - faceOffsetShape.BoundBox.ZMin))
- return (faceOffsetShape, prflShp)
- return False
-
- def _extractWiresFromFace(self, base, fc):
- '''_extractWiresFromFace(base, fc) ...
- Attempts to return all closed wires within a parent face, including the outer most wire of the parent.
- The wires are ordered by area. Each wire is also categorized as a pocket(False) or raised protrusion(True).
- '''
- PathLog.debug('_extractWiresFromFace()')
-
- WIRES = list()
- lenWrs = len(fc.Wires)
- PathLog.debug(' -Wire count: {}'.format(lenWrs))
-
- def index0(tup):
- return tup[0]
-
- # Cycle through wires in face
- for w in range(0, lenWrs):
- PathLog.debug(' -Analyzing wire_{}'.format(w + 1))
- wire = fc.Wires[w]
- checkEdges = False
- cont = True
-
- # Check for closed edges (circles, ellipses, etc...)
- for E in wire.Edges:
- if E.isClosed() is True:
- checkEdges = True
- break
-
- if checkEdges is True:
- PathLog.debug(' -checkEdges is True')
- for e in range(0, len(wire.Edges)):
- edge = wire.Edges[e]
- if edge.isClosed() is True and edge.Mass > 0.01:
- PathLog.debug(' -Found closed edge')
- raised = False
- ip = self._isPocket(base, fc, edge)
- if ip is False:
- raised = True
- ebb = edge.BoundBox
- eArea = ebb.XLength * ebb.YLength
- F = Part.Face(Part.Wire([edge]))
- WIRES.append((eArea, F.Wires[0], raised))
- cont = False
-
- if cont is True:
- PathLog.debug(' -cont is True')
- # If only one wire and not checkEdges, return first wire
- if lenWrs == 1:
- return [(wire, False)]
-
- raised = False
- wbb = wire.BoundBox
- wArea = wbb.XLength * wbb.YLength
- if w > 0:
- ip = self._isPocket(base, fc, wire)
- if ip is False:
- raised = True
- WIRES.append((wArea, Part.Wire(wire.Edges), raised))
-
- nf = len(WIRES)
- if nf > 0:
- PathLog.debug(' -number of wires found is {}'.format(nf))
- if nf == 1:
- (area, W, raised) = WIRES[0]
- return [(W, raised)]
- else:
- sortedWIRES = sorted(WIRES, key=index0, reverse=True)
- return [(W, raised) for (area, W, raised) in sortedWIRES] # outer, then inner by area size
-
- return False
-
- def _calculateOffsetValue(self, obj, isHole, isVoid=False):
- '''_calculateOffsetValue(obj, isHole, isVoid) ... internal function.
- Calculate the offset for the Path.Area() function.'''
- JOB = PathUtils.findParentJob(obj)
- tolrnc = JOB.GeometryTolerance.Value
-
- if isVoid is False:
- if isHole is True:
- offset = -1 * obj.InternalFeaturesAdjustment.Value
- offset += self.radius # (self.radius + (tolrnc / 10.0))
- else:
- offset = -1 * obj.BoundaryAdjustment.Value
- if obj.BoundaryEnforcement is True:
- offset += self.radius # (self.radius + (tolrnc / 10.0))
- else:
- offset -= self.radius # (self.radius + (tolrnc / 10.0))
- offset = 0.0 - offset
- else:
- offset = -1 * obj.BoundaryAdjustment.Value
- offset += self.radius # (self.radius + (tolrnc / 10.0))
-
- return offset
-
- def _extractFaceOffset(self, obj, fcShape, offset, makeComp=True):
- '''_extractFaceOffset(fcShape, offset) ... internal function.
- Original _buildPathArea() version copied from PathAreaOp.py module. This version is modified.
- Adjustments made based on notes by @sliptonic at this webpage: https://github.com/sliptonic/FreeCAD/wiki/PathArea-notes.'''
- PathLog.debug('_extractFaceOffset()')
-
- if fcShape.BoundBox.ZMin != 0.0:
- fcShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - fcShape.BoundBox.ZMin))
-
- areaParams = {}
- areaParams['Offset'] = offset
- areaParams['Fill'] = 1 # 1
- areaParams['Coplanar'] = 0
- areaParams['SectionCount'] = 1 # -1 = full(all per depthparams??) sections
- areaParams['Reorient'] = True
- areaParams['OpenMode'] = 0
- areaParams['MaxArcPoints'] = 400 # 400
- areaParams['Project'] = True
- # areaParams['Tolerance'] = 0.001
-
- area = Path.Area() # Create instance of Area() class object
- # area.setPlane(PathUtils.makeWorkplane(fcShape)) # Set working plane
- area.setPlane(PathUtils.makeWorkplane(self.wpc)) # Set working plane to normal at Z=1
- area.add(fcShape)
- area.setParams(**areaParams) # set parameters
-
- # Save parameters for debugging
- # obj.AreaParams = str(area.getParams())
- # PathLog.debug("Area with params: {}".format(area.getParams()))
-
- offsetShape = area.getShape()
- wCnt = len(offsetShape.Wires)
- if wCnt == 0:
- return False
- elif wCnt == 1:
- ofstFace = Part.Face(offsetShape.Wires[0])
- if not makeComp:
- ofstFace = [ofstFace]
- else:
- W = list()
- for wr in offsetShape.Wires:
- W.append(Part.Face(wr))
- if makeComp:
- ofstFace = Part.makeCompound(W)
- else:
- ofstFace = W
-
- return ofstFace # offsetShape
-
- def _isPocket(self, b, f, w):
- '''_isPocket(b, f, w)...
- Attempts to determine if the wire(w) in face(f) of base(b) is a pocket or raised protrusion.
- Returns True if pocket, False if raised protrusion.'''
- e = w.Edges[0]
- for fi in range(0, len(b.Shape.Faces)):
- face = b.Shape.Faces[fi]
- for ei in range(0, len(face.Edges)):
- edge = face.Edges[ei]
- if e.isSame(edge) is True:
- if f is face:
- # Alternative: run loop to see if all edges are same
- pass # same source face, look for another
- else:
- if face.CenterOfMass.z < f.CenterOfMass.z:
- return True
- return False
-
- def _flattenWireToFace(self, wire):
- PathLog.debug('_flattenWireToFace()')
- if wire.isClosed() is False:
- PathLog.debug(' -wire.isClosed() is False')
- return False
-
- # If wire is planar horizontal, convert to a face and return
- if wire.BoundBox.ZLength == 0.0:
- slc = Part.Face(wire)
- return slc
-
- # Attempt to create a new wire for manipulation, if not, use original
- newWire = Part.Wire(wire.Edges)
- if newWire.isClosed() is True:
- nWire = newWire
- else:
- PathLog.debug(' -newWire.isClosed() is False')
- nWire = wire
-
- # Attempt extrusion, and then try a manual slice and then cross-section
- ext = self._getExtrudedShape(nWire)
- if ext is False:
- PathLog.debug('_getExtrudedShape() failed')
- else:
- slc = self._getShapeSlice(ext)
- if slc is not False:
- return slc
- cs = self._getCrossSection(ext, True)
- if cs is not False:
- return cs
-
- # Attempt creating an envelope, and then try a manual slice and then cross-section
- env = self._getShapeEnvelope(nWire)
- if env is False:
- PathLog.debug('_getShapeEnvelope() failed')
- else:
- slc = self._getShapeSlice(env)
- if slc is not False:
- return slc
- cs = self._getCrossSection(env, True)
- if cs is not False:
- return cs
-
- # Attempt creating a projection
- slc = self._getProjectedFace(nWire)
- if slc is False:
- PathLog.debug('_getProjectedFace() failed')
- else:
- return slc
-
- return False
-
- def _getExtrudedShape(self, wire):
- PathLog.debug('_getExtrudedShape()')
- wBB = wire.BoundBox
- extFwd = math.floor(2.0 * wBB.ZLength) + 10.0
-
- try:
- # slower, but renders collective faces correctly. Method 5 in TESTING
- shell = wire.extrude(FreeCAD.Vector(0.0, 0.0, extFwd))
- except Exception as ee:
- PathLog.error(' -extrude wire failed: \n{}'.format(ee))
- return False
-
- SHP = Part.makeSolid(shell)
- return SHP
-
- def _getShapeSlice(self, shape):
- PathLog.debug('_getShapeSlice()')
-
- bb = shape.BoundBox
- mid = (bb.ZMin + bb.ZMax) / 2.0
- xmin = bb.XMin - 1.0
- xmax = bb.XMax + 1.0
- ymin = bb.YMin - 1.0
- ymax = bb.YMax + 1.0
- p1 = FreeCAD.Vector(xmin, ymin, mid)
- p2 = FreeCAD.Vector(xmax, ymin, mid)
- p3 = FreeCAD.Vector(xmax, ymax, mid)
- p4 = FreeCAD.Vector(xmin, ymax, mid)
-
- e1 = Part.makeLine(p1, p2)
- e2 = Part.makeLine(p2, p3)
- e3 = Part.makeLine(p3, p4)
- e4 = Part.makeLine(p4, p1)
- face = Part.Face(Part.Wire([e1, e2, e3, e4]))
- fArea = face.BoundBox.XLength * face.BoundBox.YLength # face.Wires[0].Area
- sArea = shape.BoundBox.XLength * shape.BoundBox.YLength
- midArea = (fArea + sArea) / 2.0
-
- slcShp = shape.common(face)
- slcArea = slcShp.BoundBox.XLength * slcShp.BoundBox.YLength
-
- if slcArea < midArea:
- for W in slcShp.Wires:
- if W.isClosed() is False:
- PathLog.debug(' -wire.isClosed() is False')
- return False
- if len(slcShp.Wires) == 1:
- wire = slcShp.Wires[0]
- slc = Part.Face(wire)
- slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin))
- return slc
- else:
- fL = list()
- for W in slcShp.Wires:
- slc = Part.Face(W)
- slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin))
- fL.append(slc)
- comp = Part.makeCompound(fL)
- if self.showDebugObjects is True:
- PathLog.debug('*** tmpSliceCompound')
- P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpSliceCompound')
- P.Shape = comp
- # P.recompute()
- P.purgeTouched()
- self.tempGroup.addObject(P)
- return comp
-
- PathLog.debug(' -slcArea !< midArea')
- PathLog.debug(' -slcShp.Edges count: {}. Might be a vertically oriented face.'.format(len(slcShp.Edges)))
- return False
-
- def _getProjectedFace(self, wire):
- PathLog.debug('_getProjectedFace()')
- F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpProjectionWire')
- F.Shape = wire
- F.purgeTouched()
- self.tempGroup.addObject(F)
- try:
- prj = Draft.makeShape2DView(F, FreeCAD.Vector(0, 0, 1))
- prj.recompute()
- prj.purgeTouched()
- self.tempGroup.addObject(prj)
- except Exception as ee:
- PathLog.error(str(ee))
- return False
- else:
- pWire = Part.Wire(prj.Shape.Edges)
- if pWire.isClosed() is False:
- # PathLog.debug(' -pWire.isClosed() is False')
- return False
- slc = Part.Face(pWire)
- slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin))
- return slc
- return False
-
- def _getCrossSection(self, shape, withExtrude=False):
- PathLog.debug('_getCrossSection()')
- wires = list()
- bb = shape.BoundBox
- mid = (bb.ZMin + bb.ZMax) / 2.0
-
- for i in shape.slice(FreeCAD.Vector(0, 0, 1), mid):
- wires.append(i)
-
- if len(wires) > 0:
- comp = Part.Compound(wires) # produces correct cross-section wire !
- comp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - comp.BoundBox.ZMin))
- csWire = comp.Wires[0]
- if csWire.isClosed() is False:
- PathLog.debug(' -comp.Wires[0] is not closed')
- return False
- if withExtrude is True:
- ext = self._getExtrudedShape(csWire)
- CS = self._getShapeSlice(ext)
- else:
- CS = Part.Face(csWire)
- CS.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - CS.BoundBox.ZMin))
- return CS
- else:
- PathLog.debug(' -No wires from .slice() method')
-
- return False
-
- def _getShapeEnvelope(self, shape):
- PathLog.debug('_getShapeEnvelope()')
-
- wBB = shape.BoundBox
- extFwd = wBB.ZLength + 10.0
- minz = wBB.ZMin
- maxz = wBB.ZMin + extFwd
- stpDwn = (maxz - minz) / 4.0
- dep_par = PathUtils.depth_params(maxz + 5.0, maxz + 3.0, maxz, stpDwn, 0.0, minz)
-
- try:
- env = PathUtils.getEnvelope(partshape=shape, depthparams=dep_par) # Produces .Shape
- except Exception as ee:
- PathLog.error('try: PathUtils.getEnvelope() failed.\n' + str(ee))
- return False
- else:
- return env
-
- return False
-
- def _getSliceFromEnvelope(self, env):
- PathLog.debug('_getSliceFromEnvelope()')
- eBB = env.BoundBox
- extFwd = eBB.ZLength + 10.0
- maxz = eBB.ZMin + extFwd
-
- maxMax = env.Edges[0].BoundBox.ZMin
- emax = math.floor(maxz - 1.0)
- E = list()
- for e in range(0, len(env.Edges)):
- emin = env.Edges[e].BoundBox.ZMin
- if emin > emax:
- E.append(env.Edges[e])
- tf = Part.Face(Part.Wire(Part.__sortEdges__(E)))
- tf.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - tf.BoundBox.ZMin))
-
- return tf
-
- def _prepareModelSTLs(self, JOB, obj):
- PathLog.debug('_prepareModelSTLs()')
- for m in range(0, len(JOB.Model.Group)):
- M = JOB.Model.Group[m]
-
- # PathLog.debug(f" -self.modelTypes[{m}] == 'M'")
- if self.modelTypes[m] == 'M':
- #TODO: test if this works
- facets = M.Mesh.Facets.Points
- else:
- facets = Path.getFacets(M.Shape)
-
- if self.modelSTLs[m] is True:
- stl = ocl.STLSurf()
-
- for tri in facets:
- t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2] + obj.DepthOffset.Value),
- ocl.Point(tri[1][0], tri[1][1], tri[1][2] + obj.DepthOffset.Value),
- ocl.Point(tri[2][0], tri[2][1], tri[2][2] + obj.DepthOffset.Value))
- stl.addTriangle(t)
- self.modelSTLs[m] = stl
- return
-
- def _makeSafeSTL(self, JOB, obj, mdlIdx, faceShapes, voidShapes):
- '''_makeSafeSTL(JOB, obj, mdlIdx, faceShapes, voidShapes)...
- Creates and OCL.stl object with combined data with waste stock,
- model, and avoided faces. Travel lines can be checked against this
- STL object to determine minimum travel height to clear stock and model.'''
- PathLog.debug('_makeSafeSTL()')
-
- fuseShapes = list()
- Mdl = JOB.Model.Group[mdlIdx]
- FCAD = FreeCAD.ActiveDocument
- mBB = Mdl.Shape.BoundBox
- sBB = JOB.Stock.Shape.BoundBox
-
- # add Model shape to safeSTL shape
- fuseShapes.append(Mdl.Shape)
-
- if obj.BoundBox == 'BaseBoundBox':
- cont = False
- extFwd = (sBB.ZLength)
- zmin = mBB.ZMin
- zmax = mBB.ZMin + extFwd
- stpDwn = (zmax - zmin) / 4.0
- dep_par = PathUtils.depth_params(zmax + 5.0, zmax + 3.0, zmax, stpDwn, 0.0, zmin)
-
- try:
- envBB = PathUtils.getEnvelope(partshape=Mdl.Shape, depthparams=dep_par) # Produces .Shape
- cont = True
- except Exception as ee:
- PathLog.error(str(ee))
- shell = Mdl.Shape.Shells[0]
- solid = Part.makeSolid(shell)
- try:
- envBB = PathUtils.getEnvelope(partshape=solid, depthparams=dep_par) # Produces .Shape
- cont = True
- except Exception as eee:
- PathLog.error(str(eee))
-
- if cont is True:
- stckWst = JOB.Stock.Shape.cut(envBB)
- if obj.BoundaryAdjustment > 0.0:
- cmpndFS = Part.makeCompound(faceShapes)
- baBB = PathUtils.getEnvelope(partshape=cmpndFS, depthparams=self.depthParams) # Produces .Shape
- adjStckWst = stckWst.cut(baBB)
- else:
- adjStckWst = stckWst
- fuseShapes.append(adjStckWst)
- else:
- PathLog.warning('Path transitions might not avoid the model. Verify paths.')
- # time.sleep(0.3)
-
- else:
- # If boundbox is Job.Stock, add hidden pad under stock as base plate
- toolDiam = self.cutter.getDiameter()
- zMin = JOB.Stock.Shape.BoundBox.ZMin
- xMin = JOB.Stock.Shape.BoundBox.XMin - toolDiam
- yMin = JOB.Stock.Shape.BoundBox.YMin - toolDiam
- bL = JOB.Stock.Shape.BoundBox.XLength + (2 * toolDiam)
- bW = JOB.Stock.Shape.BoundBox.YLength + (2 * toolDiam)
- bH = 1.0
- crnr = FreeCAD.Vector(xMin, yMin, zMin - 1.0)
- B = Part.makeBox(bL, bW, bH, crnr, FreeCAD.Vector(0, 0, 1))
- fuseShapes.append(B)
-
- if voidShapes is not False:
- voidComp = Part.makeCompound(voidShapes)
- voidEnv = PathUtils.getEnvelope(partshape=voidComp, depthparams=self.depthParams) # Produces .Shape
- fuseShapes.append(voidEnv)
-
- fused = Part.makeCompound(fuseShapes)
-
- if self.showDebugObjects is True:
- T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'safeSTLShape')
- T.Shape = fused
- T.purgeTouched()
- self.tempGroup.addObject(T)
-
- facets = Path.getFacets(fused)
-
- stl = ocl.STLSurf()
- for tri in facets:
- t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2]),
- ocl.Point(tri[1][0], tri[1][1], tri[1][2]),
- ocl.Point(tri[2][0], tri[2][1], tri[2][2]))
- stl.addTriangle(t)
-
- self.safeSTLs[mdlIdx] = stl
-
- def _processCutAreas(self, JOB, obj, mdlIdx, FCS, VDS):
- '''_processCutAreas(JOB, obj, mdlIdx, FCS, VDS)...
- This method applies any avoided faces or regions to the selected faces.
- It then calls the correct method.'''
- PathLog.debug('_processCutAreas()')
-
- final = list()
- base = JOB.Model.Group[mdlIdx]
-
- # Process faces Collectively or Individually
- if obj.HandleMultipleFeatures == 'Collectively':
- if FCS is True:
- COMP = False
- else:
- ADD = Part.makeCompound(FCS)
- if VDS is not False:
- DEL = Part.makeCompound(VDS)
- COMP = ADD.cut(DEL)
- else:
- COMP = ADD
-
- final.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
- if obj.Algorithm == 'OCL Dropcutter':
- final.extend(self._oclWaterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline
- else:
- final.extend(self._experimentalWaterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline
-
- elif obj.HandleMultipleFeatures == 'Individually':
- for fsi in range(0, len(FCS)):
- fShp = FCS[fsi]
- # self.deleteOpVariables(all=False)
- self.resetOpVariables(all=False)
-
- if fShp is True:
- COMP = False
- else:
- ADD = Part.makeCompound([fShp])
- if VDS is not False:
- DEL = Part.makeCompound(VDS)
- COMP = ADD.cut(DEL)
- else:
- COMP = ADD
-
- final.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
- if obj.Algorithm == 'OCL Dropcutter':
- final.extend(self._oclWaterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline
- else:
- final.extend(self._experimentalWaterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline
- COMP = None
- # Eif
-
- return final
-
- # Methods for creating path geometry
- def _planarMakePathGeom(self, obj, faceShp):
- '''_planarMakePathGeom(obj, faceShp)...
- Creates the line/arc cut pattern geometry and returns the intersection with the received faceShp.
- The resulting intersecting line/arc geometries are then converted to lines or arcs for OCL.'''
- PathLog.debug('_planarMakePathGeom()')
- GeoSet = list()
-
- # Apply drop cutter extra offset and set the max and min XY area of the operation
- xmin = faceShp.BoundBox.XMin
- xmax = faceShp.BoundBox.XMax
- ymin = faceShp.BoundBox.YMin
- ymax = faceShp.BoundBox.YMax
- zmin = faceShp.BoundBox.ZMin
- zmax = faceShp.BoundBox.ZMax
-
- # Compute weighted center of mass of all faces combined
- fCnt = 0
- totArea = 0.0
- zeroCOM = FreeCAD.Vector(0.0, 0.0, 0.0)
- for F in faceShp.Faces:
- comF = F.CenterOfMass
- areaF = F.Area
- totArea += areaF
- fCnt += 1
- zeroCOM = zeroCOM.add(FreeCAD.Vector(comF.x, comF.y, 0.0).multiply(areaF))
- if fCnt == 0:
- PathLog.error(translate('PathSurface', 'Cannot calculate the Center Of Mass. Using Center of Boundbox.'))
- zeroCOM = FreeCAD.Vector((xmin + xmax) / 2.0, (ymin + ymax) / 2.0, 0.0)
- else:
- avgArea = totArea / fCnt
- zeroCOM.multiply(1 / fCnt)
- zeroCOM.multiply(1 / avgArea)
- COM = FreeCAD.Vector(zeroCOM.x, zeroCOM.y, 0.0)
-
- # get X, Y, Z spans; Compute center of rotation
- deltaX = abs(xmax-xmin)
- deltaY = abs(ymax-ymin)
- deltaZ = abs(zmax-zmin)
- deltaC = math.sqrt(deltaX**2 + deltaY**2)
- lineLen = deltaC + (2.0 * self.cutter.getDiameter()) # Line length to span boundbox diag with 2x cutter diameter extra on each end
- halfLL = math.ceil(lineLen / 2.0)
- cutPasses = math.ceil(lineLen / self.cutOut) + 1 # Number of lines(passes) required to cover lineLen
- halfPasses = math.ceil(cutPasses / 2.0)
- bbC = faceShp.BoundBox.Center
-
- # Generate the Draft line/circle sets to be intersected with the cut-face-area
- if obj.CutPattern in ['ZigZag', 'Line']:
- MaxLC = -1
- centRot = FreeCAD.Vector(0.0, 0.0, 0.0) # Bottom left corner of face/selection/model
- cAng = math.atan(deltaX / deltaY) # BoundaryBox angle
-
- # Determine end points and create top lines
- x1 = centRot.x - halfLL
- x2 = centRot.x + halfLL
- diag = None
- if obj.CutPatternAngle == 0 or obj.CutPatternAngle == 180:
- MaxLC = math.floor(deltaY / self.cutOut)
- diag = deltaY
- elif obj.CutPatternAngle == 90 or obj.CutPatternAngle == 270:
- MaxLC = math.floor(deltaX / self.cutOut)
- diag = deltaX
- else:
- perpDist = math.cos(cAng - math.radians(obj.CutPatternAngle)) * deltaC
- MaxLC = math.floor(perpDist / self.cutOut)
- diag = perpDist
- y1 = centRot.y + diag
- # y2 = y1
-
- p1 = FreeCAD.Vector(x1, y1, 0.0)
- p2 = FreeCAD.Vector(x2, y1, 0.0)
- topLineTuple = (p1, p2)
- ny1 = centRot.y - diag
- n1 = FreeCAD.Vector(x1, ny1, 0.0)
- n2 = FreeCAD.Vector(x2, ny1, 0.0)
- negTopLineTuple = (n1, n2)
-
- # Create end points for set of lines to intersect with cross-section face
- pntTuples = list()
- for lc in range((-1 * (halfPasses - 1)), halfPasses + 1):
- # if lc == (cutPasses - MaxLC - 1):
- # pntTuples.append(negTopLineTuple)
- # if lc == (MaxLC + 1):
- # pntTuples.append(topLineTuple)
- x1 = centRot.x - halfLL
- x2 = centRot.x + halfLL
- y1 = centRot.y + (lc * self.cutOut)
- # y2 = y1
- p1 = FreeCAD.Vector(x1, y1, 0.0)
- p2 = FreeCAD.Vector(x2, y1, 0.0)
- pntTuples.append( (p1, p2) )
-
- # Convert end points to lines
- for (p1, p2) in pntTuples:
- line = Part.makeLine(p1, p2)
- GeoSet.append(line)
- elif obj.CutPattern in ['Circular', 'CircularZigZag']:
- zTgt = faceShp.BoundBox.ZMin
- axisRot = FreeCAD.Vector(0.0, 0.0, 1.0)
- cntr = FreeCAD.Placement()
- cntr.Rotation = FreeCAD.Rotation(axisRot, 0.0)
-
- if obj.CircularCenterAt == 'CenterOfMass':
- cntr.Base = FreeCAD.Vector(COM.x, COM.y, zTgt) # COM # Use center of Mass
- elif obj.CircularCenterAt == 'CenterOfBoundBox':
- cent = faceShp.BoundBox.Center
- cntr.Base = FreeCAD.Vector(cent.x, cent.y, zTgt)
- elif obj.CircularCenterAt == 'XminYmin':
- cntr.Base = FreeCAD.Vector(faceShp.BoundBox.XMin, faceShp.BoundBox.YMin, zTgt)
- elif obj.CircularCenterAt == 'Custom':
- newCent = FreeCAD.Vector(obj.CircularCenterCustom.x, obj.CircularCenterCustom.y, zTgt)
- cntr.Base = newCent
-
- # recalculate cutPasses value, if need be
- radialPasses = halfPasses
- if obj.CircularCenterAt != 'CenterOfBoundBox':
- # make 4 corners of boundbox in XY plane, find which is greatest distance to new circular center
- EBB = faceShp.BoundBox
- CORNERS = [
- FreeCAD.Vector(EBB.XMin, EBB.YMin, 0.0),
- FreeCAD.Vector(EBB.XMin, EBB.YMax, 0.0),
- FreeCAD.Vector(EBB.XMax, EBB.YMax, 0.0),
- FreeCAD.Vector(EBB.XMax, EBB.YMin, 0.0),
- ]
- dMax = 0.0
- for c in range(0, 4):
- dist = CORNERS[c].sub(cntr.Base).Length
- if dist > dMax:
- dMax = dist
- lineLen = dMax + (2.0 * self.cutter.getDiameter()) # Line length to span boundbox diag with 2x cutter diameter extra on each end
- radialPasses = math.ceil(lineLen / self.cutOut) + 1 # Number of lines(passes) required to cover lineLen
-
- # Update COM point and current CircularCenter
- if obj.CircularCenterAt != 'Custom':
- obj.CircularCenterCustom = cntr.Base
-
- minRad = self.cutter.getDiameter() * 0.45
- siX3 = 3 * obj.SampleInterval.Value
- minRadSI = (siX3 / 2.0) / math.pi
- if minRad < minRadSI:
- minRad = minRadSI
-
- # Make small center circle to start pattern
- if obj.StepOver > 50:
- circle = Part.makeCircle(minRad, cntr.Base)
- GeoSet.append(circle)
-
- for lc in range(1, radialPasses + 1):
- rad = (lc * self.cutOut)
- if rad >= minRad:
- circle = Part.makeCircle(rad, cntr.Base)
- GeoSet.append(circle)
- # Efor
- COM = cntr.Base
- # Eif
-
- if obj.CutPatternReversed is True:
- GeoSet.reverse()
-
- if faceShp.BoundBox.ZMin != 0.0:
- faceShp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - faceShp.BoundBox.ZMin))
-
- # Create compound object to bind all lines in Lineset
- geomShape = Part.makeCompound(GeoSet)
-
- # Position and rotate the Line and ZigZag geometry
- if obj.CutPattern in ['Line', 'ZigZag']:
- if obj.CutPatternAngle != 0.0:
- geomShape.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), obj.CutPatternAngle)
- geomShape.Placement.Base = FreeCAD.Vector(bbC.x, bbC.y, 0.0 - geomShape.BoundBox.ZMin)
-
- if self.showDebugObjects is True:
- F = FreeCAD.ActiveDocument.addObject('Part::Feature','tmpGeometrySet')
- F.Shape = geomShape
- F.purgeTouched()
- self.tempGroup.addObject(F)
-
- # Identify intersection of cross-section face and lineset
- cmnShape = faceShp.common(geomShape)
-
- if self.showDebugObjects is True:
- F = FreeCAD.ActiveDocument.addObject('Part::Feature','tmpPathGeometry')
- F.Shape = cmnShape
- F.purgeTouched()
- self.tempGroup.addObject(F)
-
- self.tmpCOM = FreeCAD.Vector(COM.x, COM.y, faceShp.BoundBox.ZMin)
- return cmnShape
-
- def _pathGeomToLinesPointSet(self, obj, compGeoShp):
- '''_pathGeomToLinesPointSet(obj, compGeoShp)...
- Convert a compound set of sequential line segments to directionally-oriented collinear groupings.'''
- PathLog.debug('_pathGeomToLinesPointSet()')
- # Extract intersection line segments for return value as list()
- LINES = list()
- inLine = list()
- chkGap = False
- lnCnt = 0
- ec = len(compGeoShp.Edges)
- cutClimb = self.CutClimb
- toolDiam = 2.0 * self.radius
- cpa = obj.CutPatternAngle
-
- edg0 = compGeoShp.Edges[0]
- p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y)
- p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y)
- if cutClimb is True:
- tup = (p2, p1)
- lst = FreeCAD.Vector(p1[0], p1[1], 0.0)
- else:
- tup = (p1, p2)
- lst = FreeCAD.Vector(p2[0], p2[1], 0.0)
- inLine.append(tup)
- sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point
-
- for ei in range(1, ec):
- chkGap = False
- edg = compGeoShp.Edges[ei] # Get edge for vertexes
- v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y) # vertex 0
- v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y) # vertex 1
-
- ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point
- cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (first / middle point)
- iC = sp.isOnLineSegment(ep, cp)
- if iC is True:
- inLine.append('BRK')
- chkGap = True
- else:
- if cutClimb is True:
- inLine.reverse()
- LINES.append(inLine) # Save inLine segments
- lnCnt += 1
- inLine = list() # reset collinear container
- if cutClimb is True:
- sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0)
- else:
- sp = ep
-
- if cutClimb is True:
- tup = (v2, v1)
- if chkGap is True:
- gap = abs(toolDiam - lst.sub(ep).Length)
- lst = cp
- else:
- tup = (v1, v2)
- if chkGap is True:
- gap = abs(toolDiam - lst.sub(cp).Length)
- lst = ep
-
- if chkGap is True:
- if gap < obj.GapThreshold.Value:
- b = inLine.pop() # pop off 'BRK' marker
- (vA, vB) = inLine.pop() # pop off previous line segment for combining with current
- tup = (vA, tup[1])
- self.closedGap = True
- else:
- # PathLog.debug('---- Gap: {} mm'.format(gap))
- gap = round(gap, 6)
- if gap < self.gaps[0]:
- self.gaps.insert(0, gap)
- self.gaps.pop()
- inLine.append(tup)
- # Efor
- lnCnt += 1
- if cutClimb is True:
- inLine.reverse()
- LINES.append(inLine) # Save inLine segments
-
- # Handle last inLine set, reversing it.
- if obj.CutPatternReversed is True:
- if cpa != 0.0 and cpa % 90.0 == 0.0:
- F = LINES.pop(0)
- rev = list()
- for iL in F:
- if iL == 'BRK':
- rev.append(iL)
- else:
- (p1, p2) = iL
- rev.append((p2, p1))
- rev.reverse()
- LINES.insert(0, rev)
-
- isEven = lnCnt % 2
- if isEven == 0:
- PathLog.debug('Line count is ODD.')
- else:
- PathLog.debug('Line count is even.')
-
- return LINES
-
- def _pathGeomToZigzagPointSet(self, obj, compGeoShp):
- '''_pathGeomToZigzagPointSet(obj, compGeoShp)...
- Convert a compound set of sequential line segments to directionally-oriented collinear groupings
- with a ZigZag directional indicator included for each collinear group.'''
- PathLog.debug('_pathGeomToZigzagPointSet()')
- # Extract intersection line segments for return value as list()
- LINES = list()
- inLine = list()
- lnCnt = 0
- chkGap = False
- ec = len(compGeoShp.Edges)
- toolDiam = 2.0 * self.radius
-
- if self.CutClimb is True:
- dirFlg = -1
- else:
- dirFlg = 1
-
- edg0 = compGeoShp.Edges[0]
- p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y)
- p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y)
- if dirFlg == 1:
- tup = (p1, p2)
- lst = FreeCAD.Vector(p2[0], p2[1], 0.0)
- sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point
- else:
- tup = (p2, p1)
- lst = FreeCAD.Vector(p1[0], p1[1], 0.0)
- sp = FreeCAD.Vector(p2[0], p2[1], 0.0) # start point
- inLine.append(tup)
- otr = lst
-
- for ei in range(1, ec):
- edg = compGeoShp.Edges[ei]
- v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y)
- v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y)
-
- cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (start point of segment)
- ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point
- iC = sp.isOnLineSegment(ep, cp)
- if iC is True:
- inLine.append('BRK')
- chkGap = True
- gap = abs(toolDiam - lst.sub(cp).Length)
- else:
- chkGap = False
- if dirFlg == -1:
- inLine.reverse()
- LINES.append((dirFlg, inLine))
- lnCnt += 1
- dirFlg = -1 * dirFlg # Change zig to zag
- inLine = list() # reset collinear container
- sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0)
- otr = ep
-
- lst = ep
- if dirFlg == 1:
- tup = (v1, v2)
- else:
- tup = (v2, v1)
-
- if chkGap is True:
- if gap < obj.GapThreshold.Value:
- b = inLine.pop() # pop off 'BRK' marker
- (vA, vB) = inLine.pop() # pop off previous line segment for combining with current
- if dirFlg == 1:
- tup = (vA, tup[1])
- else:
- #tup = (vA, tup[1])
- #tup = (tup[1], vA)
- tup = (tup[0], vB)
- self.closedGap = True
- else:
- gap = round(gap, 6)
- if gap < self.gaps[0]:
- self.gaps.insert(0, gap)
- self.gaps.pop()
- inLine.append(tup)
- # Efor
- lnCnt += 1
-
- # Fix directional issue with LAST line when line count is even
- isEven = lnCnt % 2
- if isEven == 0: # Changed to != with 90 degree CutPatternAngle
- PathLog.debug('Line count is even.')
- else:
- PathLog.debug('Line count is ODD.')
- dirFlg = -1 * dirFlg
- if obj.CutPatternReversed is False:
- if self.CutClimb is True:
- dirFlg = -1 * dirFlg
-
- if obj.CutPatternReversed is True:
- dirFlg = -1 * dirFlg
-
- # Handle last inLine list
- if dirFlg == 1:
- rev = list()
- for iL in inLine:
- if iL == 'BRK':
- rev.append(iL)
- else:
- (p1, p2) = iL
- rev.append((p2, p1))
-
- if obj.CutPatternReversed is False:
- rev.reverse()
- else:
- rev2 = list()
- for iL in rev:
- if iL == 'BRK':
- rev2.append(iL)
- else:
- (p1, p2) = iL
- rev2.append((p2, p1))
- rev2.reverse()
- rev = rev2
-
- LINES.append((dirFlg, rev))
- else:
- LINES.append((dirFlg, inLine))
-
- return LINES
-
- def _pathGeomToArcPointSet(self, obj, compGeoShp):
- '''_pathGeomToArcPointSet(obj, compGeoShp)...
- Convert a compound set of arcs/circles to a set of directionally-oriented arc end points
- and the corresponding center point.'''
- # Extract intersection line segments for return value as list()
- PathLog.debug('_pathGeomToArcPointSet()')
- ARCS = list()
- stpOvrEI = list()
- segEI = list()
- isSame = False
- sameRad = None
- COM = self.tmpCOM
- toolDiam = 2.0 * self.radius
- ec = len(compGeoShp.Edges)
-
- def gapDist(sp, ep):
- X = (ep[0] - sp[0])**2
- Y = (ep[1] - sp[1])**2
- Z = (ep[2] - sp[2])**2
- # return math.sqrt(X + Y + Z)
- return math.sqrt(X + Y) # the 'z' value is zero in both points
-
- # Separate arc data into Loops and Arcs
- for ei in range(0, ec):
- edg = compGeoShp.Edges[ei]
- if edg.Closed is True:
- stpOvrEI.append(('L', ei, False))
- else:
- if isSame is False:
- segEI.append(ei)
- isSame = True
- pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0)
- sameRad = pnt.sub(COM).Length
- else:
- # Check if arc is co-radial to current SEGS
- pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0)
- if abs(sameRad - pnt.sub(COM).Length) > 0.00001:
- isSame = False
-
- if isSame is True:
- segEI.append(ei)
- else:
- # Move co-radial arc segments
- stpOvrEI.append(['A', segEI, False])
- # Start new list of arc segments
- segEI = [ei]
- isSame = True
- pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0)
- sameRad = pnt.sub(COM).Length
- # Process trailing `segEI` data, if available
- if isSame is True:
- stpOvrEI.append(['A', segEI, False])
-
- # Identify adjacent arcs with y=0 start/end points that connect
- for so in range(0, len(stpOvrEI)):
- SO = stpOvrEI[so]
- if SO[0] == 'A':
- startOnAxis = list()
- endOnAxis = list()
- EI = SO[1] # list of corresponding compGeoShp.Edges indexes
-
- # Identify startOnAxis and endOnAxis arcs
- for i in range(0, len(EI)):
- ei = EI[i] # edge index
- E = compGeoShp.Edges[ei] # edge object
- if abs(COM.y - E.Vertexes[0].Y) < 0.00001:
- startOnAxis.append((i, ei, E.Vertexes[0]))
- elif abs(COM.y - E.Vertexes[1].Y) < 0.00001:
- endOnAxis.append((i, ei, E.Vertexes[1]))
-
- # Look for connections between startOnAxis and endOnAxis arcs. Consolidate data when connected
- lenSOA = len(startOnAxis)
- lenEOA = len(endOnAxis)
- if lenSOA > 0 and lenEOA > 0:
- delIdxs = list()
- lstFindIdx = 0
- for soa in range(0, lenSOA):
- (iS, eiS, vS) = startOnAxis[soa]
- for eoa in range(0, len(endOnAxis)):
- (iE, eiE, vE) = endOnAxis[eoa]
- dist = vE.X - vS.X
- if abs(dist) < 0.00001: # They connect on axis at same radius
- SO[2] = (eiE, eiS)
- break
- elif dist > 0:
- break # stop searching
- # Eif
- # Eif
- # Efor
-
- # Construct arc data tuples for OCL
- dirFlg = 1
- # cutPat = obj.CutPattern
- if self.CutClimb is False: # True yields Climb when set to Conventional
- dirFlg = -1
-
- # Cycle through stepOver data
- for so in range(0, len(stpOvrEI)):
- SO = stpOvrEI[so]
- if SO[0] == 'L': # L = Loop/Ring/Circle
- # PathLog.debug("SO[0] == 'Loop'")
- lei = SO[1] # loop Edges index
- v1 = compGeoShp.Edges[lei].Vertexes[0]
-
- # space = obj.SampleInterval.Value / 2.0
- space = 0.0000001
-
- # p1 = FreeCAD.Vector(v1.X, v1.Y, v1.Z)
- p1 = FreeCAD.Vector(v1.X, v1.Y, 0.0)
- rad = p1.sub(COM).Length
- spcRadRatio = space/rad
- if spcRadRatio < 1.0:
- tolrncAng = math.asin(spcRadRatio)
- else:
- tolrncAng = 0.9999998 * math.pi
- EX = COM.x + (rad * math.cos(tolrncAng))
- EY = v1.Y - space # rad * math.sin(tolrncAng)
-
- sp = (v1.X, v1.Y, 0.0)
- ep = (EX, EY, 0.0)
- cp = (COM.x, COM.y, 0.0)
- if dirFlg == 1:
- arc = (sp, ep, cp)
- else:
- arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction))
- ARCS.append(('L', dirFlg, [arc]))
- else: # SO[0] == 'A' A = Arc
- # PathLog.debug("SO[0] == 'Arc'")
- PRTS = list()
- EI = SO[1] # list of corresponding Edges indexes
- CONN = SO[2] # list of corresponding connected edges tuples (iE, iS)
- chkGap = False
- lst = None
-
- if CONN is not False:
- (iE, iS) = CONN
- v1 = compGeoShp.Edges[iE].Vertexes[0]
- v2 = compGeoShp.Edges[iS].Vertexes[1]
- sp = (v1.X, v1.Y, 0.0)
- ep = (v2.X, v2.Y, 0.0)
- cp = (COM.x, COM.y, 0.0)
- if dirFlg == 1:
- arc = (sp, ep, cp)
- lst = ep
- else:
- arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction))
- lst = sp
- PRTS.append(arc)
- # Pop connected edge index values from arc segments index list
- iEi = EI.index(iE)
- iSi = EI.index(iS)
- if iEi > iSi:
- EI.pop(iEi)
- EI.pop(iSi)
- else:
- EI.pop(iSi)
- EI.pop(iEi)
- if len(EI) > 0:
- PRTS.append('BRK')
- chkGap = True
- cnt = 0
- for ei in EI:
- if cnt > 0:
- PRTS.append('BRK')
- chkGap = True
- v1 = compGeoShp.Edges[ei].Vertexes[0]
- v2 = compGeoShp.Edges[ei].Vertexes[1]
- sp = (v1.X, v1.Y, 0.0)
- ep = (v2.X, v2.Y, 0.0)
- cp = (COM.x, COM.y, 0.0)
- if dirFlg == 1:
- arc = (sp, ep, cp)
- if chkGap is True:
- gap = abs(toolDiam - gapDist(lst, sp)) # abs(toolDiam - lst.sub(sp).Length)
- lst = ep
- else:
- arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction))
- if chkGap is True:
- gap = abs(toolDiam - gapDist(lst, ep)) # abs(toolDiam - lst.sub(ep).Length)
- lst = sp
- if chkGap is True:
- if gap < obj.GapThreshold.Value:
- b = PRTS.pop() # pop off 'BRK' marker
- (vA, vB, vC) = PRTS.pop() # pop off previous arc segment for combining with current
- arc = (vA, arc[1], vC)
- self.closedGap = True
- else:
- # PathLog.debug('---- Gap: {} mm'.format(gap))
- gap = round(gap, 6)
- if gap < self.gaps[0]:
- self.gaps.insert(0, gap)
- self.gaps.pop()
- PRTS.append(arc)
- cnt += 1
-
- if dirFlg == -1:
- PRTS.reverse()
-
- ARCS.append(('A', dirFlg, PRTS))
- # Eif
- if obj.CutPattern == 'CircularZigZag':
- dirFlg = -1 * dirFlg
- # Efor
-
- return ARCS
-
- def _getExperimentalWaterlinePaths(self, obj, PNTSET, csHght):
- '''_getExperimentalWaterlinePaths(obj, PNTSET, csHght)...
- Switching function for calling the appropriate path-geometry to OCL points conversion function
- for the various cut patterns.'''
- PathLog.debug('_getExperimentalWaterlinePaths()')
- SCANS = list()
-
- if obj.CutPattern == 'Line':
- stpOvr = list()
- for D in PNTSET:
- for SEG in D:
- if SEG == 'BRK':
- stpOvr.append(SEG)
- else:
- # D format is ((p1, p2), (p3, p4))
- (A, B) = SEG
- P1 = FreeCAD.Vector(A[0], A[1], csHght)
- P2 = FreeCAD.Vector(B[0], B[1], csHght)
- stpOvr.append((P1, P2))
- SCANS.append(stpOvr)
- stpOvr = list()
- elif obj.CutPattern == 'ZigZag':
- stpOvr = list()
- for (dirFlg, LNS) in PNTSET:
- for SEG in LNS:
- if SEG == 'BRK':
- stpOvr.append(SEG)
- else:
- # D format is ((p1, p2), (p3, p4))
- (A, B) = SEG
- P1 = FreeCAD.Vector(A[0], A[1], csHght)
- P2 = FreeCAD.Vector(B[0], B[1], csHght)
- stpOvr.append((P1, P2))
- SCANS.append(stpOvr)
- stpOvr = list()
- elif obj.CutPattern in ['Circular', 'CircularZigZag']:
- # PNTSET is list, by stepover.
- # Each stepover is a list containing arc/loop descriptions, (sp, ep, cp)
- for so in range(0, len(PNTSET)):
- stpOvr = list()
- erFlg = False
- (aTyp, dirFlg, ARCS) = PNTSET[so]
-
- if dirFlg == 1: # 1
- cMode = True # Climb mode
- else:
- cMode = False
-
- for a in range(0, len(ARCS)):
- Arc = ARCS[a]
- if Arc == 'BRK':
- stpOvr.append('BRK')
- else:
- (sp, ep, cp) = Arc
- S = FreeCAD.Vector(sp[0], sp[1], csHght)
- E = FreeCAD.Vector(ep[0], ep[1], csHght)
- C = FreeCAD.Vector(cp[0], cp[1], csHght)
- scan = (S, E, C, cMode)
- if scan is False:
- erFlg = True
- else:
- ##if aTyp == 'L':
- ## stpOvr.append(FreeCAD.Vector(scan[0][0].x, scan[0][0].y, scan[0][0].z))
- stpOvr.append(scan)
- if erFlg is False:
- SCANS.append(stpOvr)
-
- return SCANS
-
- # Main planar scan functions
- def _stepTransitionCmds(self, obj, lstPnt, first, minSTH, tolrnc):
- cmds = list()
- rtpd = False
- horizGC = 'G0'
- hSpeed = self.horizRapid
- height = obj.SafeHeight.Value
-
- if obj.CutPattern in ['Line', 'Circular']:
- if obj.OptimizeStepOverTransitions is True:
- height = minSTH + 2.0
- # if obj.LayerMode == 'Multi-pass':
- # rtpd = minSTH
- elif obj.CutPattern in ['ZigZag', 'CircularZigZag']:
- if obj.OptimizeStepOverTransitions is True:
- zChng = first.z - lstPnt.z
- # PathLog.debug('first.z: {}'.format(first.z))
- # PathLog.debug('lstPnt.z: {}'.format(lstPnt.z))
- # PathLog.debug('zChng: {}'.format(zChng))
- # PathLog.debug('minSTH: {}'.format(minSTH))
- if abs(zChng) < tolrnc: # transitions to same Z height
- # PathLog.debug('abs(zChng) < tolrnc')
- if (minSTH - first.z) > tolrnc:
- # PathLog.debug('(minSTH - first.z) > tolrnc')
- height = minSTH + 2.0
- else:
- # PathLog.debug('ELSE (minSTH - first.z) > tolrnc')
- horizGC = 'G1'
- height = first.z
- elif (minSTH + (2.0 * tolrnc)) >= max(first.z, lstPnt.z):
- height = False # allow end of Zig to cut to beginning of Zag
-
-
- # Create raise, shift, and optional lower commands
- if height is not False:
- cmds.append(Path.Command('G0', {'Z': height, 'F': self.vertRapid}))
- cmds.append(Path.Command(horizGC, {'X': first.x, 'Y': first.y, 'F': hSpeed}))
- if rtpd is not False: # ReturnToPreviousDepth
- cmds.append(Path.Command('G0', {'Z': rtpd, 'F': self.vertRapid}))
-
- return cmds
-
- def _breakCmds(self, obj, lstPnt, first, minSTH, tolrnc):
- cmds = list()
- rtpd = False
- horizGC = 'G0'
- hSpeed = self.horizRapid
- height = obj.SafeHeight.Value
-
- if obj.CutPattern in ['Line', 'Circular']:
- if obj.OptimizeStepOverTransitions is True:
- height = minSTH + 2.0
- elif obj.CutPattern in ['ZigZag', 'CircularZigZag']:
- if obj.OptimizeStepOverTransitions is True:
- zChng = first.z - lstPnt.z
- if abs(zChng) < tolrnc: # transitions to same Z height
- if (minSTH - first.z) > tolrnc:
- height = minSTH + 2.0
- else:
- height = first.z + 2.0 # first.z
-
- cmds.append(Path.Command('G0', {'Z': height, 'F': self.vertRapid}))
- cmds.append(Path.Command(horizGC, {'X': first.x, 'Y': first.y, 'F': hSpeed}))
- if rtpd is not False: # ReturnToPreviousDepth
- cmds.append(Path.Command('G0', {'Z': rtpd, 'F': self.vertRapid}))
-
- return cmds
-
- def _planarGetPDC(self, stl, finalDep, SampleInterval, useSafeCutter=False):
- pdc = ocl.PathDropCutter() # create a pdc [PathDropCutter] object
- pdc.setSTL(stl) # add stl model
- if useSafeCutter is True:
- pdc.setCutter(self.safeCutter) # add safeCutter
- else:
- pdc.setCutter(self.cutter) # add cutter
- pdc.setZ(finalDep) # set minimumZ (final / target depth value)
- pdc.setSampling(SampleInterval) # set sampling size
- return pdc
-
- # Main waterline functions
- def _oclWaterlineOp(self, JOB, obj, mdlIdx, subShp=None):
- '''_oclWaterlineOp(obj, base) ... Main waterline function to perform waterline extraction from model.'''
- commands = []
-
- t_begin = time.time()
- # JOB = PathUtils.findParentJob(obj)
- base = JOB.Model.Group[mdlIdx]
- bb = self.boundBoxes[mdlIdx]
- stl = self.modelSTLs[mdlIdx]
-
- # Prepare global holdpoint and layerEndPnt containers
- if self.holdPoint is None:
- self.holdPoint = ocl.Point(float("inf"), float("inf"), float("inf"))
- if self.layerEndPnt is None:
- self.layerEndPnt = ocl.Point(float("inf"), float("inf"), float("inf"))
-
- # Set extra offset to diameter of cutter to allow cutter to move around perimeter of model
- toolDiam = self.cutter.getDiameter()
- cdeoX = 0.6 * toolDiam
- cdeoY = 0.6 * toolDiam
-
- if subShp is None:
- # Get correct boundbox
- if obj.BoundBox == 'Stock':
- BS = JOB.Stock
- bb = BS.Shape.BoundBox
- elif obj.BoundBox == 'BaseBoundBox':
- BS = base
- bb = base.Shape.BoundBox
-
- env = PathUtils.getEnvelope(partshape=BS.Shape, depthparams=self.depthParams) # Produces .Shape
-
- xmin = bb.XMin
- xmax = bb.XMax
- ymin = bb.YMin
- ymax = bb.YMax
- zmin = bb.ZMin
- zmax = bb.ZMax
- else:
- xmin = subShp.BoundBox.XMin
- xmax = subShp.BoundBox.XMax
- ymin = subShp.BoundBox.YMin
- ymax = subShp.BoundBox.YMax
- zmin = subShp.BoundBox.ZMin
- zmax = subShp.BoundBox.ZMax
-
- smplInt = obj.SampleInterval.Value
- minSampInt = 0.001 # value is mm
- if smplInt < minSampInt:
- smplInt = minSampInt
-
- # Determine bounding box length for the OCL scan
- bbLength = math.fabs(ymax - ymin)
- numScanLines = int(math.ceil(bbLength / smplInt) + 1) # Number of lines
-
- # Compute number and size of stepdowns, and final depth
- if obj.LayerMode == 'Single-pass':
- depthparams = [obj.FinalDepth.Value]
- else:
- depthparams = [dp for dp in self.depthParams]
- lenDP = len(depthparams)
-
- # Prepare PathDropCutter objects with STL data
- safePDC = self._planarGetPDC(self.safeSTLs[mdlIdx],
- depthparams[lenDP - 1], obj.SampleInterval.Value, useSafeCutter=False)
-
- # Scan the piece to depth at smplInt
- oclScan = []
- oclScan = self._waterlineDropCutScan(stl, smplInt, xmin, xmax, ymin, depthparams[lenDP - 1], numScanLines)
- # oclScan = SCANS
- lenOS = len(oclScan)
- ptPrLn = int(lenOS / numScanLines)
-
- # Convert oclScan list of points to multi-dimensional list
- scanLines = []
- for L in range(0, numScanLines):
- scanLines.append([])
- for P in range(0, ptPrLn):
- pi = L * ptPrLn + P
- scanLines[L].append(oclScan[pi])
- lenSL = len(scanLines)
- pntsPerLine = len(scanLines[0])
- PathLog.debug("--OCL scan: " + str(lenSL * pntsPerLine) + " points, with " + str(numScanLines) + " lines and " + str(pntsPerLine) + " pts/line")
-
- # Extract Wl layers per depthparams
- lyr = 0
- cmds = []
- layTime = time.time()
- self.topoMap = []
- for layDep in depthparams:
- cmds = self._getWaterline(obj, scanLines, layDep, lyr, lenSL, pntsPerLine)
- commands.extend(cmds)
- lyr += 1
- PathLog.debug("--All layer scans combined took " + str(time.time() - layTime) + " s")
- return commands
-
- def _waterlineDropCutScan(self, stl, smplInt, xmin, xmax, ymin, fd, numScanLines):
- '''_waterlineDropCutScan(stl, smplInt, xmin, xmax, ymin, fd, numScanLines) ...
- Perform OCL scan for waterline purpose.'''
- pdc = ocl.PathDropCutter() # create a pdc
- pdc.setSTL(stl)
- pdc.setCutter(self.cutter)
- pdc.setZ(fd) # set minimumZ (final / target depth value)
- pdc.setSampling(smplInt)
-
- # Create line object as path
- path = ocl.Path() # create an empty path object
- for nSL in range(0, numScanLines):
- yVal = ymin + (nSL * smplInt)
- p1 = ocl.Point(xmin, yVal, fd) # start-point of line
- p2 = ocl.Point(xmax, yVal, fd) # end-point of line
- path.append(ocl.Line(p1, p2))
- # path.append(l) # add the line to the path
- pdc.setPath(path)
- pdc.run() # run drop-cutter on the path
-
- # return the list the points
- return pdc.getCLPoints()
-
- def _getWaterline(self, obj, scanLines, layDep, lyr, lenSL, pntsPerLine):
- '''_getWaterline(obj, scanLines, layDep, lyr, lenSL, pntsPerLine) ... Get waterline.'''
- commands = []
- cmds = []
- loopList = []
- self.topoMap = []
- # Create topo map from scanLines (highs and lows)
- self.topoMap = self._createTopoMap(scanLines, layDep, lenSL, pntsPerLine)
- # Add buffer lines and columns to topo map
- self._bufferTopoMap(lenSL, pntsPerLine)
- # Identify layer waterline from OCL scan
- self._highlightWaterline(4, 9)
- # Extract waterline and convert to gcode
- loopList = self._extractWaterlines(obj, scanLines, lyr, layDep)
- # save commands
- for loop in loopList:
- cmds = self._loopToGcode(obj, layDep, loop)
- commands.extend(cmds)
- return commands
-
- def _createTopoMap(self, scanLines, layDep, lenSL, pntsPerLine):
- '''_createTopoMap(scanLines, layDep, lenSL, pntsPerLine) ... Create topo map version of OCL scan data.'''
- topoMap = []
- for L in range(0, lenSL):
- topoMap.append([])
- for P in range(0, pntsPerLine):
- if scanLines[L][P].z > layDep:
- topoMap[L].append(2)
- else:
- topoMap[L].append(0)
- return topoMap
-
- def _bufferTopoMap(self, lenSL, pntsPerLine):
- '''_bufferTopoMap(lenSL, pntsPerLine) ... Add buffer boarder of zeros to all sides to topoMap data.'''
- pre = [0, 0]
- post = [0, 0]
- for p in range(0, pntsPerLine):
- pre.append(0)
- post.append(0)
- for l in range(0, lenSL):
- self.topoMap[l].insert(0, 0)
- self.topoMap[l].append(0)
- self.topoMap.insert(0, pre)
- self.topoMap.append(post)
- return True
-
- def _highlightWaterline(self, extraMaterial, insCorn):
- '''_highlightWaterline(extraMaterial, insCorn) ... Highlight the waterline data, separating from extra material.'''
- TM = self.topoMap
- lastPnt = len(TM[1]) - 1
- lastLn = len(TM) - 1
- highFlag = 0
-
- # ("--Convert parallel data to ridges")
- for lin in range(1, lastLn):
- for pt in range(1, lastPnt): # Ignore first and last points
- if TM[lin][pt] == 0:
- if TM[lin][pt + 1] == 2: # step up
- TM[lin][pt] = 1
- if TM[lin][pt - 1] == 2: # step down
- TM[lin][pt] = 1
-
- # ("--Convert perpendicular data to ridges and highlight ridges")
- for pt in range(1, lastPnt): # Ignore first and last points
- for lin in range(1, lastLn):
- if TM[lin][pt] == 0:
- highFlag = 0
- if TM[lin + 1][pt] == 2: # step up
- TM[lin][pt] = 1
- if TM[lin - 1][pt] == 2: # step down
- TM[lin][pt] = 1
- elif TM[lin][pt] == 2:
- highFlag += 1
- if highFlag == 3:
- if TM[lin - 1][pt - 1] < 2 or TM[lin - 1][pt + 1] < 2:
- highFlag = 2
- else:
- TM[lin - 1][pt] = extraMaterial
- highFlag = 2
-
- # ("--Square corners")
- for pt in range(1, lastPnt):
- for lin in range(1, lastLn):
- if TM[lin][pt] == 1: # point == 1
- cont = True
- if TM[lin + 1][pt] == 0: # forward == 0
- if TM[lin + 1][pt - 1] == 1: # forward left == 1
- if TM[lin][pt - 1] == 2: # left == 2
- TM[lin + 1][pt] = 1 # square the corner
- cont = False
-
- if cont is True and TM[lin + 1][pt + 1] == 1: # forward right == 1
- if TM[lin][pt + 1] == 2: # right == 2
- TM[lin + 1][pt] = 1 # square the corner
- cont = True
-
- if TM[lin - 1][pt] == 0: # back == 0
- if TM[lin - 1][pt - 1] == 1: # back left == 1
- if TM[lin][pt - 1] == 2: # left == 2
- TM[lin - 1][pt] = 1 # square the corner
- cont = False
-
- if cont is True and TM[lin - 1][pt + 1] == 1: # back right == 1
- if TM[lin][pt + 1] == 2: # right == 2
- TM[lin - 1][pt] = 1 # square the corner
-
- # remove inside corners
- for pt in range(1, lastPnt):
- for lin in range(1, lastLn):
- if TM[lin][pt] == 1: # point == 1
- if TM[lin][pt + 1] == 1:
- if TM[lin - 1][pt + 1] == 1 or TM[lin + 1][pt + 1] == 1:
- TM[lin][pt + 1] = insCorn
- elif TM[lin][pt - 1] == 1:
- if TM[lin - 1][pt - 1] == 1 or TM[lin + 1][pt - 1] == 1:
- TM[lin][pt - 1] = insCorn
-
- return True
-
- def _extractWaterlines(self, obj, oclScan, lyr, layDep):
- '''_extractWaterlines(obj, oclScan, lyr, layDep) ... Extract water lines from OCL scan data.'''
- srch = True
- lastPnt = len(self.topoMap[0]) - 1
- lastLn = len(self.topoMap) - 1
- maxSrchs = 5
- srchCnt = 1
- loopList = []
- loop = []
- loopNum = 0
-
- if self.CutClimb is True:
- lC = [-1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0]
- pC = [-1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1]
- else:
- lC = [1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0]
- pC = [-1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1]
-
- while srch is True:
- srch = False
- if srchCnt > maxSrchs:
- PathLog.debug("Max search scans, " + str(maxSrchs) + " reached\nPossible incomplete waterline result!")
- break
- for L in range(1, lastLn):
- for P in range(1, lastPnt):
- if self.topoMap[L][P] == 1:
- # start loop follow
- srch = True
- loopNum += 1
- loop = self._trackLoop(oclScan, lC, pC, L, P, loopNum)
- self.topoMap[L][P] = 0 # Mute the starting point
- loopList.append(loop)
- srchCnt += 1
- PathLog.debug("Search count for layer " + str(lyr) + " is " + str(srchCnt) + ", with " + str(loopNum) + " loops.")
- return loopList
-
- def _trackLoop(self, oclScan, lC, pC, L, P, loopNum):
- '''_trackLoop(oclScan, lC, pC, L, P, loopNum) ... Track the loop direction.'''
- loop = [oclScan[L - 1][P - 1]] # Start loop point list
- cur = [L, P, 1]
- prv = [L, P - 1, 1]
- nxt = [L, P + 1, 1]
- follow = True
- ptc = 0
- ptLmt = 200000
- while follow is True:
- ptc += 1
- if ptc > ptLmt:
- PathLog.debug("Loop number " + str(loopNum) + " at [" + str(nxt[0]) + ", " + str(nxt[1]) + "] pnt count exceeds, " + str(ptLmt) + ". Stopped following loop.")
- break
- nxt = self._findNextWlPoint(lC, pC, cur[0], cur[1], prv[0], prv[1]) # get next point
- loop.append(oclScan[nxt[0] - 1][nxt[1] - 1]) # add it to loop point list
- self.topoMap[nxt[0]][nxt[1]] = nxt[2] # Mute the point, if not Y stem
- if nxt[0] == L and nxt[1] == P: # check if loop complete
- follow = False
- elif nxt[0] == cur[0] and nxt[1] == cur[1]: # check if line cannot be detected
- follow = False
- prv = cur
- cur = nxt
- return loop
-
- def _findNextWlPoint(self, lC, pC, cl, cp, pl, pp):
- '''_findNextWlPoint(lC, pC, cl, cp, pl, pp) ...
- Find the next waterline point in the point cloud layer provided.'''
- dl = cl - pl
- dp = cp - pp
- num = 0
- i = 3
- s = 0
- mtch = 0
- found = False
- while mtch < 8: # check all 8 points around current point
- if lC[i] == dl:
- if pC[i] == dp:
- s = i - 3
- found = True
- # Check for y branch where current point is connection between branches
- for y in range(1, mtch):
- if lC[i + y] == dl:
- if pC[i + y] == dp:
- num = 1
- break
- break
- i += 1
- mtch += 1
- if found is False:
- # ("_findNext: No start point found.")
- return [cl, cp, num]
-
- for r in range(0, 8):
- l = cl + lC[s + r]
- p = cp + pC[s + r]
- if self.topoMap[l][p] == 1:
- return [l, p, num]
-
- # ("_findNext: No next pnt found")
- return [cl, cp, num]
-
- def _loopToGcode(self, obj, layDep, loop):
- '''_loopToGcode(obj, layDep, loop) ... Convert set of loop points to Gcode.'''
- # generate the path commands
- output = []
- optimize = obj.OptimizeLinearPaths
-
- prev = ocl.Point(float("inf"), float("inf"), float("inf"))
- nxt = ocl.Point(float("inf"), float("inf"), float("inf"))
- pnt = ocl.Point(float("inf"), float("inf"), float("inf"))
-
- # Create first point
- pnt.x = loop[0].x
- pnt.y = loop[0].y
- pnt.z = layDep
-
- # Position cutter to begin loop
- output.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid}))
- output.append(Path.Command('G0', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizRapid}))
- output.append(Path.Command('G1', {'Z': pnt.z, 'F': self.vertFeed}))
-
- lenCLP = len(loop)
- lastIdx = lenCLP - 1
- # Cycle through each point on loop
- for i in range(0, lenCLP):
- if i < lastIdx:
- nxt.x = loop[i + 1].x
- nxt.y = loop[i + 1].y
- nxt.z = layDep
- else:
- optimize = False
-
- if not optimize or not FreeCAD.Vector(prev.x, prev.y, prev.z).isOnLineSegment(FreeCAD.Vector(nxt.x, nxt.y, nxt.z), FreeCAD.Vector(pnt.x, pnt.y, pnt.z)):
- output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizFeed}))
-
- # Rotate point data
- prev.x = pnt.x
- prev.y = pnt.y
- prev.z = pnt.z
- pnt.x = nxt.x
- pnt.y = nxt.y
- pnt.z = nxt.z
-
- # Save layer end point for use in transitioning to next layer
- self.layerEndPnt.x = pnt.x
- self.layerEndPnt.y = pnt.y
- self.layerEndPnt.z = pnt.z
-
- return output
-
- # Main waterline functions
- def _experimentalWaterlineOp(self, JOB, obj, mdlIdx, subShp=None):
- '''_waterlineOp(JOB, obj, mdlIdx, subShp=None) ...
- Main waterline function to perform waterline extraction from model.'''
- PathLog.debug('_experimentalWaterlineOp()')
-
- msg = translate('PathWaterline', 'Experimental Waterline does not currently support selected faces.')
- PathLog.info('\n..... ' + msg)
-
- commands = []
- t_begin = time.time()
- base = JOB.Model.Group[mdlIdx]
- bb = self.boundBoxes[mdlIdx]
- stl = self.modelSTLs[mdlIdx]
- safeSTL = self.safeSTLs[mdlIdx]
- self.endVector = None
-
- finDep = obj.FinalDepth.Value + (self.geoTlrnc / 10.0)
- depthParams = PathUtils.depth_params(obj.ClearanceHeight.Value, obj.SafeHeight.Value, obj.StartDepth.Value, obj.StepDown.Value, 0.0, finDep)
-
- # Compute number and size of stepdowns, and final depth
- if obj.LayerMode == 'Single-pass':
- depthparams = [finDep]
- else:
- depthparams = [dp for dp in depthParams]
- lenDP = len(depthparams)
- PathLog.debug('Experimental Waterline depthparams:\n{}'.format(depthparams))
-
- # Prepare PathDropCutter objects with STL data
- # safePDC = self._planarGetPDC(safeSTL, depthparams[lenDP - 1], obj.SampleInterval.Value, useSafeCutter=False)
-
- buffer = self.cutter.getDiameter() * 2.0
- borderFace = Part.Face(self._makeExtendedBoundBox(JOB.Stock.Shape.BoundBox, buffer, 0.0))
-
- # Get correct boundbox
- if obj.BoundBox == 'Stock':
- stockEnv = self._getShapeEnvelope(JOB.Stock.Shape)
- bbFace = self._getCrossSection(stockEnv) # returned at Z=0.0
- elif obj.BoundBox == 'BaseBoundBox':
- baseEnv = self._getShapeEnvelope(base.Shape)
- bbFace = self._getCrossSection(baseEnv) # returned at Z=0.0
-
- trimFace = borderFace.cut(bbFace)
- if self.showDebugObjects is True:
- TF = FreeCAD.ActiveDocument.addObject('Part::Feature', 'trimFace')
- TF.Shape = trimFace
- TF.purgeTouched()
- self.tempGroup.addObject(TF)
-
- # Cycle through layer depths
- CUTAREAS = self._getCutAreas(base.Shape, depthparams, bbFace, trimFace, borderFace)
- if not CUTAREAS:
- PathLog.error('No cross-section cut areas identified.')
- return commands
-
- caCnt = 0
- ofst = obj.BoundaryAdjustment.Value
- ofst -= self.radius # (self.radius + (tolrnc / 10.0))
- caLen = len(CUTAREAS)
- lastCA = caLen - 1
- lastClearArea = None
- lastCsHght = None
- clearLastLayer = True
- for ca in range(0, caLen):
- area = CUTAREAS[ca]
- csHght = area.BoundBox.ZMin
- csHght += obj.DepthOffset.Value
- cont = False
- caCnt += 1
- if area.Area > 0.0:
- cont = True
- caWireCnt = len(area.Wires) - 1 # first wire is boundFace wire
- PathLog.debug('cutAreaWireCnt: {}'.format(caWireCnt))
- if self.showDebugObjects is True:
- CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'cutArea_{}'.format(caCnt))
- CA.Shape = area
- CA.purgeTouched()
- self.tempGroup.addObject(CA)
- else:
- PathLog.error('Cut area at {} is zero.'.format(round(csHght, 4)))
-
- # get offset wire(s) based upon cross-section cut area
- if cont:
- area.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - area.BoundBox.ZMin))
- activeArea = area.cut(trimFace)
- activeAreaWireCnt = len(activeArea.Wires) # first wire is boundFace wire
- PathLog.debug('activeAreaWireCnt: {}'.format(activeAreaWireCnt))
- if self.showDebugObjects is True:
- CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'activeArea_{}'.format(caCnt))
- CA.Shape = activeArea
- CA.purgeTouched()
- self.tempGroup.addObject(CA)
- ofstArea = self._extractFaceOffset(obj, activeArea, ofst, makeComp=False)
- if not ofstArea:
- PathLog.error('No offset area returned for cut area depth: {}'.format(csHght))
- cont = False
-
- if cont:
- # Identify solid areas in the offset data
- ofstSolidFacesList = self._getSolidAreasFromPlanarFaces(ofstArea)
- if ofstSolidFacesList:
- clearArea = Part.makeCompound(ofstSolidFacesList)
- if self.showDebugObjects is True:
- CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'clearArea_{}'.format(caCnt))
- CA.Shape = clearArea
- CA.purgeTouched()
- self.tempGroup.addObject(CA)
- else:
- cont = False
- PathLog.error('ofstSolids is False.')
-
- if cont:
- # Make waterline path for current CUTAREA depth (csHght)
- commands.extend(self._wiresToWaterlinePath(obj, clearArea, csHght))
- clearArea.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - clearArea.BoundBox.ZMin))
- lastClearArea = clearArea
- lastCsHght = csHght
-
- # Clear layer as needed
- (useOfst, usePat, clearLastLayer) = self._clearLayer(obj, ca, lastCA, clearLastLayer)
- ##if self.showDebugObjects is True and (usePat or useOfst):
- ## OA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'clearPatternArea_{}'.format(round(csHght, 2)))
- ## OA.Shape = clearArea
- ## OA.purgeTouched()
- ## self.tempGroup.addObject(OA)
- if usePat:
- commands.extend(self._makeCutPatternLayerPaths(JOB, obj, clearArea, csHght))
- if useOfst:
- commands.extend(self._makeOffsetLayerPaths(JOB, obj, clearArea, csHght))
- # Efor
-
- if clearLastLayer:
- (useOfst, usePat, cLL) = self._clearLayer(obj, 1, 1, False)
- clearArea.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - lastClearArea.BoundBox.ZMin))
- if usePat:
- commands.extend(self._makeCutPatternLayerPaths(JOB, obj, lastClearArea, lastCsHght))
-
- if useOfst:
- commands.extend(self._makeOffsetLayerPaths(JOB, obj, lastClearArea, lastCsHght))
-
- PathLog.info("Waterline: All layer scans combined took " + str(time.time() - t_begin) + " s")
- return commands
-
- def _getCutAreas(self, shape, depthparams, bbFace, trimFace, borderFace):
- '''_getCutAreas(JOB, shape, depthparams, bbFace, borderFace) ...
- Takes shape, depthparams and base-envelope-cross-section, and
- returns a list of cut areas - one for each depth.'''
- PathLog.debug('_getCutAreas()')
-
- CUTAREAS = list()
- lastLayComp = None
- isFirst = True
- lenDP = len(depthparams)
-
- # Cycle through layer depths
- for dp in range(0, lenDP):
- csHght = depthparams[dp]
- PathLog.debug('Depth {} is {}'.format(dp + 1, csHght))
-
- # Get slice at depth of shape
- csFaces = self._getModelCrossSection(shape, csHght) # returned at Z=0.0
- if not csFaces:
- PathLog.error('No cross-section wires at {}'.format(csHght))
- else:
- PathLog.debug('cross-section face count {}'.format(len(csFaces)))
- if len(csFaces) > 0:
- useFaces = self._getSolidAreasFromPlanarFaces(csFaces)
- else:
- useFaces = False
-
- if useFaces:
- PathLog.debug('useFacesCnt: {}'.format(len(useFaces)))
- compAdjFaces = Part.makeCompound(useFaces)
-
- if self.showDebugObjects is True:
- CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpSolids_{}'.format(dp + 1))
- CA.Shape = compAdjFaces
- CA.purgeTouched()
- self.tempGroup.addObject(CA)
-
- if isFirst:
- allPrevComp = compAdjFaces
- cutArea = borderFace.cut(compAdjFaces)
- else:
- preCutArea = borderFace.cut(compAdjFaces)
- cutArea = preCutArea.cut(allPrevComp) # cut out higher layers to avoid cutting recessed areas
- allPrevComp = allPrevComp.fuse(compAdjFaces)
- cutArea.translate(FreeCAD.Vector(0.0, 0.0, csHght - cutArea.BoundBox.ZMin))
- CUTAREAS.append(cutArea)
- isFirst = False
- else:
- PathLog.error('No waterline at depth: {} mm.'.format(csHght))
- # Efor
-
- if len(CUTAREAS) > 0:
- return CUTAREAS
-
- return False
-
- def _wiresToWaterlinePath(self, obj, ofstPlnrShp, csHght):
- PathLog.debug('_wiresToWaterlinePath()')
- commands = list()
-
- # Translate path geometry to layer height
- ofstPlnrShp.translate(FreeCAD.Vector(0.0, 0.0, csHght - ofstPlnrShp.BoundBox.ZMin))
- if self.showDebugObjects is True:
- OA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'waterlinePathArea_{}'.format(round(csHght, 2)))
- OA.Shape = ofstPlnrShp
- OA.purgeTouched()
- self.tempGroup.addObject(OA)
-
- commands.append(Path.Command('N (Cut Area {}.)'.format(round(csHght, 2))))
- for w in range(0, len(ofstPlnrShp.Wires)):
- wire = ofstPlnrShp.Wires[w]
- V = wire.Vertexes
- if obj.CutMode == 'Climb':
- lv = len(V) - 1
- startVect = FreeCAD.Vector(V[lv].X, V[lv].Y, V[lv].Z)
- else:
- startVect = FreeCAD.Vector(V[0].X, V[0].Y, V[0].Z)
-
- commands.append(Path.Command('N (Wire {}.)'.format(w)))
- (cmds, endVect) = self._wireToPath(obj, wire, startVect)
- commands.extend(cmds)
- commands.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
-
- return commands
-
- def _makeCutPatternLayerPaths(self, JOB, obj, clrAreaShp, csHght):
- PathLog.debug('_makeCutPatternLayerPaths()')
- commands = []
-
- clrAreaShp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - clrAreaShp.BoundBox.ZMin))
- pathGeom = self._planarMakePathGeom(obj, clrAreaShp)
- pathGeom.translate(FreeCAD.Vector(0.0, 0.0, csHght - pathGeom.BoundBox.ZMin))
- # clrAreaShp.translate(FreeCAD.Vector(0.0, 0.0, csHght - clrAreaShp.BoundBox.ZMin))
-
- if self.showDebugObjects is True:
- OA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'pathGeom_{}'.format(round(csHght, 2)))
- OA.Shape = pathGeom
- OA.purgeTouched()
- self.tempGroup.addObject(OA)
-
- # Convert pathGeom to gcode more efficiently
- if True:
- if obj.CutPattern == 'Offset':
- commands.extend(self._makeOffsetLayerPaths(JOB, obj, clrAreaShp, csHght))
- else:
- clrAreaShp.translate(FreeCAD.Vector(0.0, 0.0, csHght - clrAreaShp.BoundBox.ZMin))
- if obj.CutPattern == 'Line':
- pntSet = self._pathGeomToLinesPointSet(obj, pathGeom)
- elif obj.CutPattern == 'ZigZag':
- pntSet = self._pathGeomToZigzagPointSet(obj, pathGeom)
- elif obj.CutPattern in ['Circular', 'CircularZigZag']:
- pntSet = self._pathGeomToArcPointSet(obj, pathGeom)
- stpOVRS = self._getExperimentalWaterlinePaths(obj, pntSet, csHght)
- # PathLog.debug('stpOVRS:\n{}'.format(stpOVRS))
- safePDC = False
- cmds = self._clearGeomToPaths(JOB, obj, safePDC, stpOVRS, csHght)
- commands.extend(cmds)
- else:
- # Use Path.fromShape() to convert edges to paths
- for w in range(0, len(pathGeom.Edges)):
- wire = pathGeom.Edges[w]
- V = wire.Vertexes
- if obj.CutMode == 'Climb':
- lv = len(V) - 1
- startVect = FreeCAD.Vector(V[lv].X, V[lv].Y, V[lv].Z)
- else:
- startVect = FreeCAD.Vector(V[0].X, V[0].Y, V[0].Z)
-
- commands.append(Path.Command('N (Wire {}.)'.format(w)))
- (cmds, endVect) = self._wireToPath(obj, wire, startVect)
- commands.extend(cmds)
- commands.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
-
- return commands
-
- def _makeOffsetLayerPaths(self, JOB, obj, clrAreaShp, csHght):
- PathLog.debug('_makeOffsetLayerPaths()')
- PathLog.warning('Using `Offset` for clearing bottom layer.')
- cmds = list()
- # ofst = obj.BoundaryAdjustment.Value
- ofst = 0.0 - self.cutOut # - self.cutter.getDiameter() # (self.radius + (tolrnc / 10.0))
- shape = clrAreaShp
- cont = True
- cnt = 0
- while cont:
- ofstArea = self._extractFaceOffset(obj, shape, ofst, makeComp=True)
- if not ofstArea:
- PathLog.warning('No offset clearing area returned.')
- break
- for F in ofstArea.Faces:
- cmds.extend(self._wiresToWaterlinePath(obj, F, csHght))
- shape = ofstArea
- if cnt == 0:
- ofst = 0.0 - self.cutOut # self.cutter.Diameter()
- cnt += 1
- return cmds
-
- def _clearGeomToPaths(self, JOB, obj, safePDC, SCANDATA, csHght):
- PathLog.debug('_clearGeomToPaths()')
-
- GCODE = [Path.Command('N (Beginning of Single-pass layer.)', {})]
- tolrnc = JOB.GeometryTolerance.Value
- prevDepth = obj.SafeHeight.Value
- lenSCANDATA = len(SCANDATA)
- gDIR = ['G3', 'G2']
-
- if self.CutClimb is True:
- gDIR = ['G2', 'G3']
-
- # Send cutter to x,y position of first point on first line
- first = SCANDATA[0][0][0] # [step][item][point]
- GCODE.append(Path.Command('G0', {'X': first.x, 'Y': first.y, 'F': self.horizRapid}))
-
- # Cycle through step-over sections (line segments or arcs)
- odd = True
- lstStpEnd = None
- prevDepth = obj.SafeHeight.Value # Not used for Single-pass
- for so in range(0, lenSCANDATA):
- cmds = list()
- PRTS = SCANDATA[so]
- lenPRTS = len(PRTS)
- first = PRTS[0][0] # first point of arc/line stepover group
- start = PRTS[0][0] # will change with each line/arc segment
- last = None
- cmds.append(Path.Command('N (Begin step {}.)'.format(so), {}))
-
- if so > 0:
- if obj.CutPattern == 'CircularZigZag':
- if odd is True:
- odd = False
- else:
- odd = True
- # minTrnsHght = self._getMinSafeTravelHeight(safePDC, lstStpEnd, first) # Check safe travel height against fullSTL
- minTrnsHght = obj.SafeHeight.Value
- # cmds.append(Path.Command('N (Transition: last, first: {}, {}: minSTH: {})'.format(lstStpEnd, first, minTrnsHght), {}))
- cmds.extend(self._stepTransitionCmds(obj, lstStpEnd, first, minTrnsHght, tolrnc))
-
- # Cycle through current step-over parts
- for i in range(0, lenPRTS):
- prt = PRTS[i]
- lenPrt = len(prt)
- # PathLog.debug('prt: {}'.format(prt))
- if prt == 'BRK':
- nxtStart = PRTS[i + 1][0]
- # minSTH = self._getMinSafeTravelHeight(safePDC, last, nxtStart) # Check safe travel height against fullSTL
- minSTH = obj.SafeHeight.Value
- cmds.append(Path.Command('N (Break)', {}))
- cmds.extend(self._breakCmds(obj, last, nxtStart, minSTH, tolrnc))
- else:
- cmds.append(Path.Command('N (part {}.)'.format(i + 1), {}))
- if obj.CutPattern in ['Line', 'ZigZag']:
- start, last = prt
- cmds.append(Path.Command('G1', {'X': start.x, 'Y': start.y, 'Z': start.z, 'F': self.horizFeed}))
- cmds.append(Path.Command('G1', {'X': last.x, 'Y': last.y, 'F': self.horizFeed}))
- elif obj.CutPattern in ['Circular', 'CircularZigZag']:
- start, last, centPnt, cMode = prt
- gcode = self._makeGcodeArc(start, last, odd, gDIR, tolrnc)
- cmds.extend(gcode)
- cmds.append(Path.Command('N (End of step {}.)'.format(so), {}))
- GCODE.extend(cmds) # save line commands
- lstStpEnd = last
- # Efor
-
- return GCODE
-
- def _getSolidAreasFromPlanarFaces(self, csFaces):
- PathLog.debug('_getSolidAreasFromPlanarFaces()')
- holds = list()
- cutFaces = list()
- useFaces = list()
- lenCsF = len(csFaces)
- PathLog.debug('lenCsF: {}'.format(lenCsF))
-
- if lenCsF == 1:
- useFaces = csFaces
- else:
- fIds = list()
- aIds = list()
- pIds = list()
- cIds = list()
-
- for af in range(0, lenCsF):
- fIds.append(af) # face ids
- aIds.append(af) # face ids
- pIds.append(-1) # parent ids
- cIds.append(False) # cut ids
- holds.append(False)
-
- while len(fIds) > 0:
- li = fIds.pop()
- low = csFaces[li] # senior face
- pIds = self._idInternalFeature(csFaces, fIds, pIds, li, low)
- # Ewhile
- ##PathLog.info('fIds: {}'.format(fIds))
- ##PathLog.info('pIds: {}'.format(pIds))
-
- for af in range(lenCsF - 1, -1, -1): # cycle from last item toward first
- ##PathLog.info('af: {}'.format(af))
- prnt = pIds[af]
- ##PathLog.info('prnt: {}'.format(prnt))
- if prnt == -1:
- stack = -1
- else:
- stack = [af]
- # get_face_ids_to_parent
- stack.insert(0, prnt)
- nxtPrnt = pIds[prnt]
- # find af value for nxtPrnt
- while nxtPrnt != -1:
- stack.insert(0, nxtPrnt)
- nxtPrnt = pIds[nxtPrnt]
- cIds[af] = stack
- # PathLog.debug('cIds: {}\n'.format(cIds))
-
- for af in range(0, lenCsF):
- # PathLog.debug('af is {}'.format(af))
- pFc = cIds[af]
- if pFc == -1:
- # Simple, independent region
- holds[af] = csFaces[af] # place face in hold
- # PathLog.debug('pFc == -1')
- else:
- # Compound region
- # PathLog.debug('pFc is not -1')
- cnt = len(pFc)
- if cnt % 2.0 == 0.0:
- # even is donut cut
- # PathLog.debug('cnt is even')
- inr = pFc[cnt - 1]
- otr = pFc[cnt - 2]
- # PathLog.debug('inr / otr: {} / {}'.format(inr, otr))
- holds[otr] = holds[otr].cut(csFaces[inr])
- else:
- # odd is floating solid
- # PathLog.debug('cnt is ODD')
- holds[af] = csFaces[af]
- # Efor
-
- for af in range(0, lenCsF):
- if holds[af]:
- useFaces.append(holds[af]) # save independent solid
-
- # Eif
-
- if len(useFaces) > 0:
- return useFaces
-
- return False
-
- def _getModelCrossSection(self, shape, csHght):
- PathLog.debug('_getCrossSection()')
- wires = list()
-
- def byArea(fc):
- return fc.Area
-
- for i in shape.slice(FreeCAD.Vector(0, 0, 1), csHght):
- wires.append(i)
-
- if len(wires) > 0:
- for w in wires:
- if w.isClosed() is False:
- return False
- FCS = list()
- for w in wires:
- w.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - w.BoundBox.ZMin))
- FCS.append(Part.Face(w))
- FCS.sort(key=byArea, reverse=True)
- return FCS
- else:
- PathLog.debug(' -No wires from .slice() method')
-
- return False
-
- def _isInBoundBox(self, outShp, inShp):
- obb = outShp.BoundBox
- ibb = inShp.BoundBox
-
- if obb.XMin < ibb.XMin:
- if obb.XMax > ibb.XMax:
- if obb.YMin < ibb.YMin:
- if obb.YMax > ibb.YMax:
- return True
- return False
-
- def _idInternalFeature(self, csFaces, fIds, pIds, li, low):
- Ids = list()
- for i in fIds:
- Ids.append(i)
- while len(Ids) > 0:
- hi = Ids.pop()
- high = csFaces[hi]
- if self._isInBoundBox(high, low):
- cmn = high.common(low)
- if cmn.Area > 0.0:
- pIds[li] = hi
- break
- # Ewhile
- return pIds
-
- def _wireToPath(self, obj, wire, startVect):
- '''_wireToPath(obj, wire, startVect) ... wire to path.'''
- PathLog.track()
-
- paths = []
- pathParams = {} # pylint: disable=assignment-from-no-return
- V = wire.Vertexes
-
- pathParams['shapes'] = [wire]
- pathParams['feedrate'] = self.horizFeed
- pathParams['feedrate_v'] = self.vertFeed
- pathParams['verbose'] = True
- pathParams['resume_height'] = obj.SafeHeight.Value
- pathParams['retraction'] = obj.ClearanceHeight.Value
- pathParams['return_end'] = True
- # Note that emitting preambles between moves breaks some dressups and prevents path optimization on some controllers
- pathParams['preamble'] = False
- pathParams['start'] = startVect
-
- (pp, end_vector) = Path.fromShapes(**pathParams)
- paths.extend(pp.Commands)
- # PathLog.debug('pp: {}, end vector: {}'.format(pp, end_vector))
-
- self.endVector = end_vector # pylint: disable=attribute-defined-outside-init
-
- return (paths, end_vector)
-
- def _makeExtendedBoundBox(self, wBB, bbBfr, zDep):
- pl = FreeCAD.Placement()
- pl.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 0)
- pl.Base = FreeCAD.Vector(0, 0, 0)
-
- p1 = FreeCAD.Vector(wBB.XMin - bbBfr, wBB.YMin - bbBfr, zDep)
- p2 = FreeCAD.Vector(wBB.XMax + bbBfr, wBB.YMin - bbBfr, zDep)
- p3 = FreeCAD.Vector(wBB.XMax + bbBfr, wBB.YMax + bbBfr, zDep)
- p4 = FreeCAD.Vector(wBB.XMin - bbBfr, wBB.YMax + bbBfr, zDep)
- bb = Part.makePolygon([p1, p2, p3, p4, p1])
-
- return bb
-
- def _makeGcodeArc(self, strtPnt, endPnt, odd, gDIR, tolrnc):
- cmds = list()
- isCircle = False
- inrPnt = None
- gdi = 0
- if odd is True:
- gdi = 1
-
- # Test if pnt set is circle
- if abs(strtPnt.x - endPnt.x) < tolrnc:
- if abs(strtPnt.y - endPnt.y) < tolrnc:
- isCircle = True
- isCircle = False
-
- if isCircle is True:
- # convert LN to G2/G3 arc, consolidating GCode
- # https://wiki.shapeoko.com/index.php/G-Code#G2_-_clockwise_arc
- # https://www.cnccookbook.com/cnc-g-code-arc-circle-g02-g03/
- # Dividing circle into two arcs allows for G2/G3 on inclined surfaces
-
- # ijk = self.tmpCOM - strtPnt # vector from start to center
- ijk = self.tmpCOM - strtPnt # vector from start to center
- xyz = self.tmpCOM.add(ijk) # end point
- cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed}))
- cmds.append(Path.Command(gDIR[gdi], {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z,
- 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height
- 'F': self.horizFeed}))
- cmds.append(Path.Command('G1', {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z, 'F': self.horizFeed}))
- ijk = self.tmpCOM - xyz # vector from start to center
- rst = strtPnt # end point
- cmds.append(Path.Command(gDIR[gdi], {'X': rst.x, 'Y': rst.y, 'Z': rst.z,
- 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height
- 'F': self.horizFeed}))
- cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed}))
- else:
- # ijk = self.tmpCOM - strtPnt
- ijk = self.tmpCOM.sub(strtPnt) # vector from start to center
- xyz = endPnt
- cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed}))
- cmds.append(Path.Command(gDIR[gdi], {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z,
- 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height
- 'F': self.horizFeed}))
- cmds.append(Path.Command('G1', {'X': endPnt.x, 'Y': endPnt.y, 'Z': endPnt.z, 'F': self.horizFeed}))
-
- return cmds
-
- def _clearLayer(self, obj, ca, lastCA, clearLastLayer):
- PathLog.debug('_clearLayer()')
- usePat = False
- useOfst = False
-
- if obj.ClearLastLayer == 'Off':
- if obj.CutPattern != 'None':
- usePat = True
- else:
- if ca == lastCA:
- PathLog.debug('... Clearing bottom layer.')
- if obj.ClearLastLayer == 'Offset':
- obj.CutPattern = 'None'
- useOfst = True
- else:
- obj.CutPattern = obj.ClearLastLayer
- usePat = True
- clearLastLayer = False
-
- return (useOfst, usePat, clearLastLayer)
-
- def resetOpVariables(self, all=True):
- '''resetOpVariables() ... Reset class variables used for instance of operation.'''
- self.holdPoint = None
- self.layerEndPnt = None
- self.onHold = False
- self.SafeHeightOffset = 2.0
- self.ClearHeightOffset = 4.0
- self.layerEndzMax = 0.0
- self.resetTolerance = 0.0
- self.holdPntCnt = 0
- self.bbRadius = 0.0
- self.axialFeed = 0.0
- self.axialRapid = 0.0
- self.FinalDepth = 0.0
- self.clearHeight = 0.0
- self.safeHeight = 0.0
- self.faceZMax = -999999999999.0
- if all is True:
- self.cutter = None
- self.stl = None
- self.fullSTL = None
- self.cutOut = 0.0
- self.radius = 0.0
- self.useTiltCutter = False
- return True
-
- def deleteOpVariables(self, all=True):
- '''deleteOpVariables() ... Reset class variables used for instance of operation.'''
- del self.holdPoint
- del self.layerEndPnt
- del self.onHold
- del self.SafeHeightOffset
- del self.ClearHeightOffset
- del self.layerEndzMax
- del self.resetTolerance
- del self.holdPntCnt
- del self.bbRadius
- del self.axialFeed
- del self.axialRapid
- del self.FinalDepth
- del self.clearHeight
- del self.safeHeight
- del self.faceZMax
- if all is True:
- del self.cutter
- del self.stl
- del self.fullSTL
- del self.cutOut
- del self.radius
- del self.useTiltCutter
- return True
-
- def setOclCutter(self, obj, safe=False):
- ''' setOclCutter(obj) ... Translation function to convert FreeCAD tool definition to OCL formatted tool. '''
- # Set cutter details
- # https://www.freecadweb.org/api/dd/dfe/classPath_1_1Tool.html#details
- diam_1 = float(obj.ToolController.Tool.Diameter)
- lenOfst = obj.ToolController.Tool.LengthOffset if hasattr(obj.ToolController.Tool, 'LengthOffset') else 0
- FR = obj.ToolController.Tool.FlatRadius if hasattr(obj.ToolController.Tool, 'FlatRadius') else 0
- CEH = obj.ToolController.Tool.CuttingEdgeHeight if hasattr(obj.ToolController.Tool, 'CuttingEdgeHeight') else 0
- CEA = obj.ToolController.Tool.CuttingEdgeAngle if hasattr(obj.ToolController.Tool, 'CuttingEdgeAngle') else 0
-
- # Make safeCutter with 2 mm buffer around physical cutter
- if safe is True:
- diam_1 += 4.0
- if FR != 0.0:
- FR += 2.0
-
- PathLog.debug('ToolType: {}'.format(obj.ToolController.Tool.ToolType))
- if obj.ToolController.Tool.ToolType == 'EndMill':
- # Standard End Mill
- return ocl.CylCutter(diam_1, (CEH + lenOfst))
-
- elif obj.ToolController.Tool.ToolType == 'BallEndMill' and FR == 0.0:
- # Standard Ball End Mill
- # OCL -> BallCutter::BallCutter(diameter, length)
- self.useTiltCutter = True
- return ocl.BallCutter(diam_1, (diam_1 / 2 + lenOfst))
-
- elif obj.ToolController.Tool.ToolType == 'BallEndMill' and FR > 0.0:
- # Bull Nose or Corner Radius cutter
- # Reference: https://www.fine-tools.com/halbstabfraeser.html
- # OCL -> BallCutter::BallCutter(diameter, length)
- return ocl.BullCutter(diam_1, FR, (CEH + lenOfst))
-
- elif obj.ToolController.Tool.ToolType == 'Engraver' and FR > 0.0:
- # Bull Nose or Corner Radius cutter
- # Reference: https://www.fine-tools.com/halbstabfraeser.html
- # OCL -> ConeCutter::ConeCutter(diameter, angle, lengthOffset)
- return ocl.ConeCutter(diam_1, (CEA / 2), lenOfst)
-
- elif obj.ToolController.Tool.ToolType == 'ChamferMill':
- # Bull Nose or Corner Radius cutter
- # Reference: https://www.fine-tools.com/halbstabfraeser.html
- # OCL -> ConeCutter::ConeCutter(diameter, angle, lengthOffset)
- return ocl.ConeCutter(diam_1, (CEA / 2), lenOfst)
- else:
- # Default to standard end mill
- PathLog.warning("Defaulting cutter to standard end mill.")
- return ocl.CylCutter(diam_1, (CEH + lenOfst))
-
- # http://www.carbidecutter.net/products/carbide-burr-cone-shape-sm.html
- '''
- # Available FreeCAD cutter types - some still need translation to available OCL cutter classes.
- Drill, CenterDrill, CounterSink, CounterBore, FlyCutter, Reamer, Tap,
- EndMill, SlotCutter, BallEndMill, ChamferMill, CornerRound, Engraver
- '''
- # Adittional problem is with new ToolBit user-defined cutter shapes.
- # Some sort of translation/conversion will have to be defined to make compatible with OCL.
- PathLog.error('Unable to set OCL cutter.')
- return False
-
-
-def SetupProperties():
- ''' SetupProperties() ... Return list of properties required for operation.'''
- setup = []
- setup.append('Algorithm')
- setup.append('AngularDeflection')
- setup.append('AvoidLastX_Faces')
- setup.append('AvoidLastX_InternalFeatures')
- setup.append('BoundBox')
- setup.append('BoundaryAdjustment')
- setup.append('CircularCenterAt')
- setup.append('CircularCenterCustom')
- setup.append('ClearLastLayer')
- setup.append('CutMode')
- setup.append('CutPattern')
- setup.append('CutPatternAngle')
- setup.append('CutPatternReversed')
- setup.append('DepthOffset')
- setup.append('GapSizes')
- setup.append('GapThreshold')
- setup.append('HandleMultipleFeatures')
- setup.append('InternalFeaturesCut')
- setup.append('InternalFeaturesAdjustment')
- setup.append('LayerMode')
- setup.append('LinearDeflection')
- setup.append('OptimizeStepOverTransitions')
- setup.append('ProfileEdges')
- setup.append('BoundaryEnforcement')
- setup.append('SampleInterval')
- setup.append('StartPoint')
- setup.append('StepOver')
- setup.append('UseStartPoint')
- # For debugging
- setup.append('ShowTempObjects')
- return setup
-
-
-def Create(name, obj=None):
- '''Create(name) ... Creates and returns a Waterline operation.'''
- if obj is None:
- obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
- obj.Proxy = ObjectWaterline(obj, name)
- return obj
+# -*- coding: utf-8 -*-
+
+# ***************************************************************************
+# * *
+# * Copyright (c) 2019 Russell Johnson (russ4262) *
+# * Copyright (c) 2019 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 *
+# * *
+# ***************************************************************************
+
+from __future__ import print_function
+
+__title__ = "Path Waterline Operation"
+__author__ = "russ4262 (Russell Johnson), sliptonic (Brad Collette)"
+__url__ = "http://www.freecadweb.org"
+__doc__ = "Class and implementation of Waterline operation."
+__contributors__ = ""
+
+import FreeCAD
+from PySide import QtCore
+
+# OCL must be installed
+try:
+ import ocl
+except ImportError:
+ msg = QtCore.QCoreApplication.translate("PathWaterline", "This operation requires OpenCamLib to be installed.")
+ FreeCAD.Console.PrintError(msg + "\n")
+ raise ImportError
+ # import sys
+ # sys.exit(msg)
+
+import Path
+import PathScripts.PathLog as PathLog
+import PathScripts.PathUtils as PathUtils
+import PathScripts.PathOp as PathOp
+import PathScripts.PathSurfaceSupport as PathSurfaceSupport
+import time
+import math
+
+# lazily loaded modules
+from lazy_loader.lazy_loader import LazyLoader
+MeshPart = LazyLoader('MeshPart', globals(), 'MeshPart')
+Part = LazyLoader('Part', globals(), 'Part')
+
+if FreeCAD.GuiUp:
+ import FreeCADGui
+
+PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
+# PathLog.trackModule(PathLog.thisModule())
+
+
+# Qt translation handling
+def translate(context, text, disambig=None):
+ return QtCore.QCoreApplication.translate(context, text, disambig)
+
+
+class ObjectWaterline(PathOp.ObjectOp):
+ '''Proxy object for Surfacing operation.'''
+
+ def baseObject(self):
+ '''baseObject() ... returns super of receiver
+ Used to call base implementation in overwritten functions.'''
+ return super(self.__class__, self)
+
+ def opFeatures(self, obj):
+ '''opFeatures(obj) ... return all standard features and edges based geomtries'''
+ return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureHeights | PathOp.FeatureStepDown | PathOp.FeatureCoolant | PathOp.FeatureBaseFaces
+
+ def initOperation(self, obj):
+ '''initPocketOp(obj) ...
+ Initialize the operation - property creation and property editor status.'''
+ self.initOpProperties(obj)
+
+ # For debugging
+ if PathLog.getLevel(PathLog.thisModule()) != 4:
+ obj.setEditorMode('ShowTempObjects', 2) # hide
+
+ if not hasattr(obj, 'DoNotSetDefaultValues'):
+ self.setEditorProperties(obj)
+
+ def initOpProperties(self, obj, warn=False):
+ '''initOpProperties(obj) ... create operation specific properties'''
+ missing = list()
+
+ for (prtyp, nm, grp, tt) in self.opProperties():
+ if not hasattr(obj, nm):
+ obj.addProperty(prtyp, nm, grp, tt)
+ missing.append(nm)
+ if warn:
+ newPropMsg = translate('PathWaterline', 'New property added to') + ' "{}": '.format(obj.Label) + nm + '. '
+ newPropMsg += translate('PathWaterline', 'Check its default value.')
+ PathLog.warning(newPropMsg)
+
+ # Set enumeration lists for enumeration properties
+ if len(missing) > 0:
+ ENUMS = self.propertyEnumerations()
+ for n in ENUMS:
+ if n in missing:
+ setattr(obj, n, ENUMS[n])
+
+ self.addedAllProperties = True
+
+ def opProperties(self):
+ '''opProperties() ... return list of tuples containing operation specific properties'''
+ return [
+ ("App::PropertyBool", "ShowTempObjects", "Debug",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Show the temporary path construction objects when module is in DEBUG mode.")),
+
+ ("App::PropertyDistance", "AngularDeflection", "Mesh Conversion",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate the mesh. Smaller values increase processing time a lot.")),
+ ("App::PropertyDistance", "LinearDeflection", "Mesh Conversion",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate the mesh. Smaller values do not increase processing time much.")),
+
+ ("App::PropertyInteger", "AvoidLastX_Faces", "Selected Geometry Settings",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Avoid cutting the last 'N' faces in the Base Geometry list of selected faces.")),
+ ("App::PropertyBool", "AvoidLastX_InternalFeatures", "Selected Geometry Settings",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Do not cut internal features on avoided faces.")),
+ ("App::PropertyDistance", "BoundaryAdjustment", "Selected Geometry Settings",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Positive values push the cutter toward, or beyond, the boundary. Negative values retract the cutter away from the boundary.")),
+ ("App::PropertyBool", "BoundaryEnforcement", "Selected Geometry Settings",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "If true, the cutter will remain inside the boundaries of the model or selected face(s).")),
+ ("App::PropertyEnumeration", "HandleMultipleFeatures", "Selected Geometry Settings",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose how to process multiple Base Geometry features.")),
+ ("App::PropertyDistance", "InternalFeaturesAdjustment", "Selected Geometry Settings",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Positive values push the cutter toward, or into, the feature. Negative values retract the cutter away from the feature.")),
+ ("App::PropertyBool", "InternalFeaturesCut", "Selected Geometry Settings",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Ignore internal feature areas within a larger selected face.")),
+
+ ("App::PropertyEnumeration", "Algorithm", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Select the algorithm to use: OCL Dropcutter*, or Experimental (Not OCL based).")),
+ ("App::PropertyEnumeration", "BoundBox", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Select the overall boundary for the operation.")),
+ ("App::PropertyEnumeration", "ClearLastLayer", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Set to clear last layer in a `Multi-pass` operation.")),
+ ("App::PropertyEnumeration", "CutMode", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the direction for the cutting tool to engage the material: Climb (ClockWise) or Conventional (CounterClockWise)")),
+ ("App::PropertyEnumeration", "CutPattern", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the geometric clearing pattern to use for the operation.")),
+ ("App::PropertyFloat", "CutPatternAngle", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "The yaw angle used for certain clearing patterns")),
+ ("App::PropertyBool", "CutPatternReversed", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Reverse the cut order of the stepover paths. For circular cut patterns, begin at the outside and work toward the center.")),
+ ("App::PropertyDistance", "DepthOffset", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the Z-axis depth offset from the target surface.")),
+ ("App::PropertyDistance", "IgnoreOuterAbove", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Ignore outer waterlines above this height.")),
+ ("App::PropertyEnumeration", "LayerMode", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Complete the operation in a single pass at depth, or mulitiple passes to final depth.")),
+ ("App::PropertyVectorDistance", "PatternCenterCustom", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the start point for the cut pattern.")),
+ ("App::PropertyEnumeration", "PatternCenterAt", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose location of the center point for starting the cut pattern.")),
+ ("App::PropertyDistance", "SampleInterval", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the sampling resolution. Smaller values quickly increase processing time.")),
+ ("App::PropertyPercent", "StepOver", "Clearing Options",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the stepover percentage, based on the tool's diameter.")),
+
+ ("App::PropertyBool", "OptimizeLinearPaths", "Optimization",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable optimization of linear paths (co-linear points). Removes unnecessary co-linear points from G-Code output.")),
+ ("App::PropertyBool", "OptimizeStepOverTransitions", "Optimization",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable separate optimization of transitions between, and breaks within, each step over path.")),
+ ("App::PropertyDistance", "GapThreshold", "Optimization",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Collinear and co-radial artifact gaps that are smaller than this threshold are closed in the path.")),
+ ("App::PropertyString", "GapSizes", "Optimization",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Feedback: three smallest gaps identified in the path geometry.")),
+
+ ("App::PropertyVectorDistance", "StartPoint", "Start Point",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "The custom start point for the path of this operation")),
+ ("App::PropertyBool", "UseStartPoint", "Start Point",
+ QtCore.QT_TRANSLATE_NOOP("App::Property", "Make True, if specifying a Start Point"))
+ ]
+
+ def propertyEnumerations(self):
+ # Enumeration lists for App::PropertyEnumeration properties
+ return {
+ 'Algorithm': ['OCL Dropcutter', 'Experimental'],
+ 'BoundBox': ['BaseBoundBox', 'Stock'],
+ 'PatternCenterAt': ['CenterOfMass', 'CenterOfBoundBox', 'XminYmin', 'Custom'],
+ 'ClearLastLayer': ['Off', 'Line', 'Circular', 'CircularZigZag', 'Offset', 'Spiral', 'ZigZag'],
+ 'CutMode': ['Conventional', 'Climb'],
+ 'CutPattern': ['None', 'Line', 'Circular', 'CircularZigZag', 'Offset', 'Spiral', 'ZigZag'], # Additional goals ['Offset', 'Spiral', 'ZigZagOffset', 'Grid', 'Triangle']
+ 'HandleMultipleFeatures': ['Collectively', 'Individually'],
+ 'LayerMode': ['Single-pass', 'Multi-pass'],
+ }
+
+ def setEditorProperties(self, obj):
+ # Used to hide inputs in properties list
+ expMode = G = 0
+ show = hide = A = B = C = 2
+ if hasattr(obj, 'EnableRotation'):
+ obj.setEditorMode('EnableRotation', hide)
+
+ obj.setEditorMode('BoundaryEnforcement', hide)
+ obj.setEditorMode('InternalFeaturesAdjustment', hide)
+ obj.setEditorMode('InternalFeaturesCut', hide)
+ obj.setEditorMode('AvoidLastX_Faces', hide)
+ obj.setEditorMode('AvoidLastX_InternalFeatures', hide)
+ obj.setEditorMode('BoundaryAdjustment', hide)
+ obj.setEditorMode('HandleMultipleFeatures', hide)
+ obj.setEditorMode('OptimizeLinearPaths', hide)
+ obj.setEditorMode('OptimizeStepOverTransitions', hide)
+ obj.setEditorMode('GapThreshold', hide)
+ obj.setEditorMode('GapSizes', hide)
+
+ if obj.Algorithm == 'OCL Dropcutter':
+ pass
+ elif obj.Algorithm == 'Experimental':
+ A = B = C = 0
+ expMode = G = show = hide = 2
+
+ cutPattern = obj.CutPattern
+ if obj.ClearLastLayer != 'Off':
+ cutPattern = obj.ClearLastLayer
+
+ if cutPattern == 'None':
+ show = hide = A = 2
+ elif cutPattern in ['Line', 'ZigZag']:
+ show = 0
+ elif cutPattern in ['Circular', 'CircularZigZag']:
+ show = 2 # hide
+ hide = 0 # show
+ elif cutPattern == 'Spiral':
+ G = hide = 0
+
+ obj.setEditorMode('CutPatternAngle', show)
+ obj.setEditorMode('PatternCenterAt', hide)
+ obj.setEditorMode('PatternCenterCustom', hide)
+ obj.setEditorMode('CutPatternReversed', A)
+
+ obj.setEditorMode('ClearLastLayer', C)
+ obj.setEditorMode('StepOver', B)
+ obj.setEditorMode('IgnoreOuterAbove', B)
+ obj.setEditorMode('CutPattern', C)
+ obj.setEditorMode('SampleInterval', G)
+ obj.setEditorMode('LinearDeflection', expMode)
+ obj.setEditorMode('AngularDeflection', expMode)
+
+ def onChanged(self, obj, prop):
+ if hasattr(self, 'addedAllProperties'):
+ if self.addedAllProperties is True:
+ if prop in ['Algorithm', 'CutPattern']:
+ self.setEditorProperties(obj)
+
+ def opOnDocumentRestored(self, obj):
+ self.initOpProperties(obj, warn=True)
+
+ if PathLog.getLevel(PathLog.thisModule()) != 4:
+ obj.setEditorMode('ShowTempObjects', 2) # hide
+ else:
+ obj.setEditorMode('ShowTempObjects', 0) # show
+
+ # Repopulate enumerations in case of changes
+ ENUMS = self.propertyEnumerations()
+ for n in ENUMS:
+ restore = False
+ if hasattr(obj, n):
+ val = obj.getPropertyByName(n)
+ restore = True
+ setattr(obj, n, ENUMS[n])
+ if restore:
+ setattr(obj, n, val)
+
+ self.setEditorProperties(obj)
+
+ def opSetDefaultValues(self, obj, job):
+ '''opSetDefaultValues(obj, job) ... initialize defaults'''
+ job = PathUtils.findParentJob(obj)
+
+ obj.OptimizeLinearPaths = True
+ obj.InternalFeaturesCut = True
+ obj.OptimizeStepOverTransitions = False
+ obj.BoundaryEnforcement = True
+ obj.UseStartPoint = False
+ obj.AvoidLastX_InternalFeatures = True
+ obj.CutPatternReversed = False
+ obj.IgnoreOuterAbove = obj.StartDepth.Value + 0.00001
+ obj.StartPoint = FreeCAD.Vector(0.0, 0.0, obj.ClearanceHeight.Value)
+ obj.Algorithm = 'OCL Dropcutter'
+ obj.LayerMode = 'Single-pass'
+ obj.CutMode = 'Conventional'
+ obj.CutPattern = 'None'
+ obj.HandleMultipleFeatures = 'Collectively' # 'Individually'
+ obj.PatternCenterAt = 'CenterOfMass' # 'CenterOfBoundBox', 'XminYmin', 'Custom'
+ obj.GapSizes = 'No gaps identified.'
+ obj.ClearLastLayer = 'Off'
+ obj.StepOver = 100
+ obj.CutPatternAngle = 0.0
+ obj.DepthOffset.Value = 0.0
+ obj.SampleInterval.Value = 1.0
+ obj.BoundaryAdjustment.Value = 0.0
+ obj.InternalFeaturesAdjustment.Value = 0.0
+ obj.AvoidLastX_Faces = 0
+ obj.PatternCenterCustom = FreeCAD.Vector(0.0, 0.0, 0.0)
+ obj.GapThreshold.Value = 0.005
+ obj.LinearDeflection.Value = 0.0001
+ obj.AngularDeflection.Value = 0.25
+ # For debugging
+ obj.ShowTempObjects = False
+
+ # need to overwrite the default depth calculations for facing
+ d = None
+ if job:
+ if job.Stock:
+ d = PathUtils.guessDepths(job.Stock.Shape, None)
+ obj.IgnoreOuterAbove = job.Stock.Shape.BoundBox.ZMax + 0.000001
+ PathLog.debug("job.Stock exists")
+ else:
+ PathLog.debug("job.Stock NOT exist")
+ else:
+ PathLog.debug("job NOT exist")
+
+ if d is not None:
+ obj.OpFinalDepth.Value = d.final_depth
+ obj.OpStartDepth.Value = d.start_depth
+ else:
+ obj.OpFinalDepth.Value = -10
+ obj.OpStartDepth.Value = 10
+
+ PathLog.debug('Default OpFinalDepth: {}'.format(obj.OpFinalDepth.Value))
+ PathLog.debug('Defualt OpStartDepth: {}'.format(obj.OpStartDepth.Value))
+
+ def opApplyPropertyLimits(self, obj):
+ '''opApplyPropertyLimits(obj) ... Apply necessary limits to user input property values before performing main operation.'''
+ # Limit sample interval
+ if obj.SampleInterval.Value < 0.0001:
+ obj.SampleInterval.Value = 0.0001
+ PathLog.error(translate('PathWaterline', 'Sample interval limits are 0.0001 to 25.4 millimeters.'))
+ if obj.SampleInterval.Value > 25.4:
+ obj.SampleInterval.Value = 25.4
+ PathLog.error(translate('PathWaterline', 'Sample interval limits are 0.0001 to 25.4 millimeters.'))
+
+ # Limit cut pattern angle
+ if obj.CutPatternAngle < -360.0:
+ obj.CutPatternAngle = 0.0
+ PathLog.error(translate('PathWaterline', 'Cut pattern angle limits are +-360 degrees.'))
+ if obj.CutPatternAngle >= 360.0:
+ obj.CutPatternAngle = 0.0
+ PathLog.error(translate('PathWaterline', 'Cut pattern angle limits are +- 360 degrees.'))
+
+ # Limit StepOver to natural number percentage
+ if obj.StepOver > 100:
+ obj.StepOver = 100
+ if obj.StepOver < 1:
+ obj.StepOver = 1
+
+ # Limit AvoidLastX_Faces to zero and positive values
+ if obj.AvoidLastX_Faces < 0:
+ obj.AvoidLastX_Faces = 0
+ PathLog.error(translate('PathWaterline', 'AvoidLastX_Faces: Only zero or positive values permitted.'))
+ if obj.AvoidLastX_Faces > 100:
+ obj.AvoidLastX_Faces = 100
+ PathLog.error(translate('PathWaterline', 'AvoidLastX_Faces: Avoid last X faces count limited to 100.'))
+
+ def opExecute(self, obj):
+ '''opExecute(obj) ... process surface operation'''
+ PathLog.track()
+
+ self.modelSTLs = list()
+ self.safeSTLs = list()
+ self.modelTypes = list()
+ self.boundBoxes = list()
+ self.profileShapes = list()
+ self.collectiveShapes = list()
+ self.individualShapes = list()
+ self.avoidShapes = list()
+ self.geoTlrnc = None
+ self.tempGroup = None
+ self.CutClimb = False
+ self.closedGap = False
+ self.tmpCOM = None
+ self.gaps = [0.1, 0.2, 0.3]
+ CMDS = list()
+ modelVisibility = list()
+ FCAD = FreeCAD.ActiveDocument
+
+ try:
+ dotIdx = __name__.index('.') + 1
+ except Exception:
+ dotIdx = 0
+ self.module = __name__[dotIdx:]
+
+ # make circle for workplane
+ self.wpc = Part.makeCircle(2.0)
+
+ # Set debugging behavior
+ self.showDebugObjects = False # Set to true if you want a visual DocObjects created for some path construction objects
+ self.showDebugObjects = obj.ShowTempObjects
+ deleteTempsFlag = True # Set to False for debugging
+ if PathLog.getLevel(PathLog.thisModule()) == 4:
+ deleteTempsFlag = False
+ else:
+ self.showDebugObjects = False
+
+ # mark beginning of operation and identify parent Job
+ PathLog.info('\nBegin Waterline operation...')
+ startTime = time.time()
+
+ # Identify parent Job
+ JOB = PathUtils.findParentJob(obj)
+ if JOB is None:
+ PathLog.error(translate('PathWaterline', "No JOB"))
+ return
+ self.stockZMin = JOB.Stock.Shape.BoundBox.ZMin
+
+ # set cut mode; reverse as needed
+ if obj.CutMode == 'Climb':
+ self.CutClimb = True
+ if obj.CutPatternReversed is True:
+ if self.CutClimb is True:
+ self.CutClimb = False
+ else:
+ self.CutClimb = True
+
+ # Begin GCode for operation with basic information
+ # ... and move cutter to clearance height and startpoint
+ output = ''
+ if obj.Comment != '':
+ self.commandlist.append(Path.Command('N ({})'.format(str(obj.Comment)), {}))
+ self.commandlist.append(Path.Command('N ({})'.format(obj.Label), {}))
+ self.commandlist.append(Path.Command('N (Tool type: {})'.format(str(obj.ToolController.Tool.ToolType)), {}))
+ self.commandlist.append(Path.Command('N (Compensated Tool Path. Diameter: {})'.format(str(obj.ToolController.Tool.Diameter)), {}))
+ self.commandlist.append(Path.Command('N (Sample interval: {})'.format(str(obj.SampleInterval.Value)), {}))
+ self.commandlist.append(Path.Command('N (Step over %: {})'.format(str(obj.StepOver)), {}))
+ self.commandlist.append(Path.Command('N ({})'.format(output), {}))
+ self.commandlist.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid}))
+ if obj.UseStartPoint:
+ self.commandlist.append(Path.Command('G0', {'X': obj.StartPoint.x, 'Y': obj.StartPoint.y, 'F': self.horizRapid}))
+
+ # Instantiate additional class operation variables
+ self.resetOpVariables()
+
+ # Impose property limits
+ self.opApplyPropertyLimits(obj)
+
+ # Create temporary group for temporary objects, removing existing
+ # if self.showDebugObjects is True:
+ tempGroupName = 'tempPathWaterlineGroup'
+ if FCAD.getObject(tempGroupName):
+ for to in FCAD.getObject(tempGroupName).Group:
+ FCAD.removeObject(to.Name)
+ FCAD.removeObject(tempGroupName) # remove temp directory if already exists
+ if FCAD.getObject(tempGroupName + '001'):
+ for to in FCAD.getObject(tempGroupName + '001').Group:
+ FCAD.removeObject(to.Name)
+ FCAD.removeObject(tempGroupName + '001') # remove temp directory if already exists
+ tempGroup = FCAD.addObject('App::DocumentObjectGroup', tempGroupName)
+ tempGroupName = tempGroup.Name
+ self.tempGroup = tempGroup
+ tempGroup.purgeTouched()
+ # Add temp object to temp group folder with following code:
+ # ... self.tempGroup.addObject(OBJ)
+
+ # Setup cutter for OCL and cutout value for operation - based on tool controller properties
+ self.cutter = self.setOclCutter(obj)
+ if self.cutter is False:
+ PathLog.error(translate('PathWaterline', "Canceling Waterline operation. Error creating OCL cutter."))
+ return
+ self.toolDiam = self.cutter.getDiameter()
+ self.radius = self.toolDiam / 2.0
+ self.cutOut = (self.toolDiam * (float(obj.StepOver) / 100.0))
+ self.gaps = [self.toolDiam, self.toolDiam, self.toolDiam]
+
+ # Get height offset values for later use
+ self.SafeHeightOffset = JOB.SetupSheet.SafeHeightOffset.Value
+ self.ClearHeightOffset = JOB.SetupSheet.ClearanceHeightOffset.Value
+
+ # Set deflection values for mesh generation
+ useDGT = False
+ try: # try/except is for Path Jobs created before GeometryTolerance
+ self.geoTlrnc = JOB.GeometryTolerance.Value
+ if self.geoTlrnc == 0.0:
+ useDGT = True
+ except AttributeError as ee:
+ PathLog.warning('{}\nPlease set Job.GeometryTolerance to an acceptable value. Using PathPreferences.defaultGeometryTolerance().'.format(ee))
+ useDGT = True
+ if useDGT:
+ import PathScripts.PathPreferences as PathPreferences
+ self.geoTlrnc = PathPreferences.defaultGeometryTolerance()
+
+ # Calculate default depthparams for operation
+ self.depthParams = PathUtils.depth_params(obj.ClearanceHeight.Value, obj.SafeHeight.Value, obj.StartDepth.Value, obj.StepDown.Value, 0.0, obj.FinalDepth.Value)
+ self.midDep = (obj.StartDepth.Value + obj.FinalDepth.Value) / 2.0
+
+ # Save model visibilities for restoration
+ if FreeCAD.GuiUp:
+ for m in range(0, len(JOB.Model.Group)):
+ mNm = JOB.Model.Group[m].Name
+ modelVisibility.append(FreeCADGui.ActiveDocument.getObject(mNm).Visibility)
+
+ # Setup STL, model type, and bound box containers for each model in Job
+ for m in range(0, len(JOB.Model.Group)):
+ M = JOB.Model.Group[m]
+ self.modelSTLs.append(False)
+ self.safeSTLs.append(False)
+ self.profileShapes.append(False)
+ # Set bound box
+ if obj.BoundBox == 'BaseBoundBox':
+ if M.TypeId.startswith('Mesh'):
+ self.modelTypes.append('M') # Mesh
+ self.boundBoxes.append(M.Mesh.BoundBox)
+ else:
+ self.modelTypes.append('S') # Solid
+ self.boundBoxes.append(M.Shape.BoundBox)
+ elif obj.BoundBox == 'Stock':
+ self.modelTypes.append('S') # Solid
+ self.boundBoxes.append(JOB.Stock.Shape.BoundBox)
+
+ # ###### MAIN COMMANDS FOR OPERATION ######
+
+ # Begin processing obj.Base data and creating GCode
+ PSF = PathSurfaceSupport.ProcessSelectedFaces(JOB, obj)
+ PSF.setShowDebugObjects(tempGroup, self.showDebugObjects)
+ PSF.radius = self.radius
+ PSF.depthParams = self.depthParams
+ pPM = PSF.preProcessModel(self.module)
+ # Process selected faces, if available
+ if pPM is False:
+ PathLog.error('Unable to pre-process obj.Base.')
+ else:
+ (FACES, VOIDS) = pPM
+ self.modelSTLs = PSF.modelSTLs
+ self.profileShapes = PSF.profileShapes
+
+ # Create OCL.stl model objects
+ if obj.Algorithm == 'OCL Dropcutter':
+ self._prepareModelSTLs(JOB, obj)
+ PathLog.debug('obj.LinearDeflection.Value: {}'.format(obj.LinearDeflection.Value))
+ PathLog.debug('obj.AngularDeflection.Value: {}'.format(obj.AngularDeflection.Value))
+
+ for m in range(0, len(JOB.Model.Group)):
+ Mdl = JOB.Model.Group[m]
+ if FACES[m] is False:
+ PathLog.error('No data for model base: {}'.format(JOB.Model.Group[m].Label))
+ else:
+ if m > 0:
+ # Raise to clearance between models
+ CMDS.append(Path.Command('N (Transition to base: {}.)'.format(Mdl.Label)))
+ CMDS.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid}))
+ PathLog.info('Working on Model.Group[{}]: {}'.format(m, Mdl.Label))
+ # make stock-model-voidShapes STL model for avoidance detection on transitions
+ if obj.Algorithm == 'OCL Dropcutter':
+ self._makeSafeSTL(JOB, obj, m, FACES[m], VOIDS[m])
+ # Process model/faces - OCL objects must be ready
+ CMDS.extend(self._processWaterlineAreas(JOB, obj, m, FACES[m], VOIDS[m]))
+
+ # Save gcode produced
+ self.commandlist.extend(CMDS)
+
+ # ###### CLOSING COMMANDS FOR OPERATION ######
+
+ # Delete temporary objects
+ # Restore model visibilities for restoration
+ if FreeCAD.GuiUp:
+ FreeCADGui.ActiveDocument.getObject(tempGroupName).Visibility = False
+ for m in range(0, len(JOB.Model.Group)):
+ M = JOB.Model.Group[m]
+ M.Visibility = modelVisibility[m]
+
+ if deleteTempsFlag is True:
+ for to in tempGroup.Group:
+ if hasattr(to, 'Group'):
+ for go in to.Group:
+ FCAD.removeObject(go.Name)
+ FCAD.removeObject(to.Name)
+ FCAD.removeObject(tempGroupName)
+ else:
+ if len(tempGroup.Group) == 0:
+ FCAD.removeObject(tempGroupName)
+ else:
+ tempGroup.purgeTouched()
+
+ # Provide user feedback for gap sizes
+ gaps = list()
+ for g in self.gaps:
+ if g != self.toolDiam:
+ gaps.append(g)
+ if len(gaps) > 0:
+ obj.GapSizes = '{} mm'.format(gaps)
+ else:
+ if self.closedGap is True:
+ obj.GapSizes = 'Closed gaps < Gap Threshold.'
+ else:
+ obj.GapSizes = 'No gaps identified.'
+
+ # clean up class variables
+ self.resetOpVariables()
+ self.deleteOpVariables()
+
+ self.modelSTLs = None
+ self.safeSTLs = None
+ self.modelTypes = None
+ self.boundBoxes = None
+ self.gaps = None
+ self.closedGap = None
+ self.SafeHeightOffset = None
+ self.ClearHeightOffset = None
+ self.depthParams = None
+ self.midDep = None
+ del self.modelSTLs
+ del self.safeSTLs
+ del self.modelTypes
+ del self.boundBoxes
+ del self.gaps
+ del self.closedGap
+ del self.SafeHeightOffset
+ del self.ClearHeightOffset
+ del self.depthParams
+ del self.midDep
+
+ execTime = time.time() - startTime
+ PathLog.info('Operation time: {} sec.'.format(execTime))
+
+ return True
+
+ # Methods for constructing the cut area
+ def _prepareModelSTLs(self, JOB, obj):
+ PathLog.debug('_prepareModelSTLs()')
+ for m in range(0, len(JOB.Model.Group)):
+ M = JOB.Model.Group[m]
+
+ # PathLog.debug(f" -self.modelTypes[{m}] == 'M'")
+ if self.modelTypes[m] == 'M':
+ # TODO: test if this works
+ facets = M.Mesh.Facets.Points
+ else:
+ facets = Part.getFacets(M.Shape)
+
+ if self.modelSTLs[m] is True:
+ stl = ocl.STLSurf()
+
+ for tri in facets:
+ t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2]),
+ ocl.Point(tri[1][0], tri[1][1], tri[1][2]),
+ ocl.Point(tri[2][0], tri[2][1], tri[2][2]))
+ stl.addTriangle(t)
+ self.modelSTLs[m] = stl
+ return
+
+ def _makeSafeSTL(self, JOB, obj, mdlIdx, faceShapes, voidShapes):
+ '''_makeSafeSTL(JOB, obj, mdlIdx, faceShapes, voidShapes)...
+ Creates and OCL.stl object with combined data with waste stock,
+ model, and avoided faces. Travel lines can be checked against this
+ STL object to determine minimum travel height to clear stock and model.'''
+ PathLog.debug('_makeSafeSTL()')
+
+ fuseShapes = list()
+ Mdl = JOB.Model.Group[mdlIdx]
+ mBB = Mdl.Shape.BoundBox
+ sBB = JOB.Stock.Shape.BoundBox
+
+ # add Model shape to safeSTL shape
+ fuseShapes.append(Mdl.Shape)
+
+ if obj.BoundBox == 'BaseBoundBox':
+ cont = False
+ extFwd = (sBB.ZLength)
+ zmin = mBB.ZMin
+ zmax = mBB.ZMin + extFwd
+ stpDwn = (zmax - zmin) / 4.0
+ dep_par = PathUtils.depth_params(zmax + 5.0, zmax + 3.0, zmax, stpDwn, 0.0, zmin)
+
+ try:
+ envBB = PathUtils.getEnvelope(partshape=Mdl.Shape, depthparams=dep_par) # Produces .Shape
+ cont = True
+ except Exception as ee:
+ PathLog.error(str(ee))
+ shell = Mdl.Shape.Shells[0]
+ solid = Part.makeSolid(shell)
+ try:
+ envBB = PathUtils.getEnvelope(partshape=solid, depthparams=dep_par) # Produces .Shape
+ cont = True
+ except Exception as eee:
+ PathLog.error(str(eee))
+
+ if cont:
+ stckWst = JOB.Stock.Shape.cut(envBB)
+ if obj.BoundaryAdjustment > 0.0:
+ cmpndFS = Part.makeCompound(faceShapes)
+ baBB = PathUtils.getEnvelope(partshape=cmpndFS, depthparams=self.depthParams) # Produces .Shape
+ adjStckWst = stckWst.cut(baBB)
+ else:
+ adjStckWst = stckWst
+ fuseShapes.append(adjStckWst)
+ else:
+ PathLog.warning('Path transitions might not avoid the model. Verify paths.')
+ else:
+ # If boundbox is Job.Stock, add hidden pad under stock as base plate
+ toolDiam = self.cutter.getDiameter()
+ zMin = JOB.Stock.Shape.BoundBox.ZMin
+ xMin = JOB.Stock.Shape.BoundBox.XMin - toolDiam
+ yMin = JOB.Stock.Shape.BoundBox.YMin - toolDiam
+ bL = JOB.Stock.Shape.BoundBox.XLength + (2 * toolDiam)
+ bW = JOB.Stock.Shape.BoundBox.YLength + (2 * toolDiam)
+ bH = 1.0
+ crnr = FreeCAD.Vector(xMin, yMin, zMin - 1.0)
+ B = Part.makeBox(bL, bW, bH, crnr, FreeCAD.Vector(0, 0, 1))
+ fuseShapes.append(B)
+
+ if voidShapes is not False:
+ voidComp = Part.makeCompound(voidShapes)
+ voidEnv = PathUtils.getEnvelope(partshape=voidComp, depthparams=self.depthParams) # Produces .Shape
+ fuseShapes.append(voidEnv)
+
+ fused = Part.makeCompound(fuseShapes)
+
+ if self.showDebugObjects is True:
+ T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'safeSTLShape')
+ T.Shape = fused
+ T.purgeTouched()
+ self.tempGroup.addObject(T)
+
+ facets = Part.getFacets(fused)
+
+ stl = ocl.STLSurf()
+ for tri in facets:
+ t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2]),
+ ocl.Point(tri[1][0], tri[1][1], tri[1][2]),
+ ocl.Point(tri[2][0], tri[2][1], tri[2][2]))
+ stl.addTriangle(t)
+
+ self.safeSTLs[mdlIdx] = stl
+
+ def _processWaterlineAreas(self, JOB, obj, mdlIdx, FCS, VDS):
+ '''_processWaterlineAreas(JOB, obj, mdlIdx, FCS, VDS)...
+ This method applies any avoided faces or regions to the selected faces.
+ It then calls the correct method.'''
+ PathLog.debug('_processWaterlineAreas()')
+
+ final = list()
+
+ # Process faces Collectively or Individually
+ if obj.HandleMultipleFeatures == 'Collectively':
+ if FCS is True:
+ COMP = False
+ else:
+ ADD = Part.makeCompound(FCS)
+ if VDS is not False:
+ DEL = Part.makeCompound(VDS)
+ COMP = ADD.cut(DEL)
+ else:
+ COMP = ADD
+
+ final.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
+ if obj.Algorithm == 'OCL Dropcutter':
+ final.extend(self._oclWaterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline
+ else:
+ final.extend(self._experimentalWaterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline
+
+ elif obj.HandleMultipleFeatures == 'Individually':
+ for fsi in range(0, len(FCS)):
+ fShp = FCS[fsi]
+ # self.deleteOpVariables(all=False)
+ self.resetOpVariables(all=False)
+
+ if fShp is True:
+ COMP = False
+ else:
+ ADD = Part.makeCompound([fShp])
+ if VDS is not False:
+ DEL = Part.makeCompound(VDS)
+ COMP = ADD.cut(DEL)
+ else:
+ COMP = ADD
+
+ final.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
+ if obj.Algorithm == 'OCL Dropcutter':
+ final.extend(self._oclWaterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline
+ else:
+ final.extend(self._experimentalWaterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline
+ COMP = None
+ # Eif
+
+ return final
+
+ # Methods for creating path geometry
+ def _getExperimentalWaterlinePaths(self, PNTSET, csHght, cutPattern):
+ '''_getExperimentalWaterlinePaths(PNTSET, csHght, cutPattern)...
+ Switching function for calling the appropriate path-geometry to OCL points conversion function
+ for the various cut patterns.'''
+ PathLog.debug('_getExperimentalWaterlinePaths()')
+ SCANS = list()
+
+ if cutPattern in ['Line', 'Spiral']:
+ stpOvr = list()
+ for D in PNTSET:
+ for SEG in D:
+ if SEG == 'BRK':
+ stpOvr.append(SEG)
+ else:
+ # D format is ((p1, p2), (p3, p4))
+ (A, B) = SEG
+ P1 = FreeCAD.Vector(A[0], A[1], csHght)
+ P2 = FreeCAD.Vector(B[0], B[1], csHght)
+ stpOvr.append((P1, P2))
+ SCANS.append(stpOvr)
+ stpOvr = list()
+ elif cutPattern == 'ZigZag':
+ stpOvr = list()
+ for (dirFlg, LNS) in PNTSET:
+ for SEG in LNS:
+ if SEG == 'BRK':
+ stpOvr.append(SEG)
+ else:
+ # D format is ((p1, p2), (p3, p4))
+ (A, B) = SEG
+ P1 = FreeCAD.Vector(A[0], A[1], csHght)
+ P2 = FreeCAD.Vector(B[0], B[1], csHght)
+ stpOvr.append((P1, P2))
+ SCANS.append(stpOvr)
+ stpOvr = list()
+ elif cutPattern in ['Circular', 'CircularZigZag']:
+ # PNTSET is list, by stepover.
+ # Each stepover is a list containing arc/loop descriptions, (sp, ep, cp)
+ for so in range(0, len(PNTSET)):
+ stpOvr = list()
+ erFlg = False
+ (aTyp, dirFlg, ARCS) = PNTSET[so]
+
+ if dirFlg == 1: # 1
+ cMode = True # Climb mode
+ else:
+ cMode = False
+
+ for a in range(0, len(ARCS)):
+ Arc = ARCS[a]
+ if Arc == 'BRK':
+ stpOvr.append('BRK')
+ else:
+ (sp, ep, cp) = Arc
+ S = FreeCAD.Vector(sp[0], sp[1], csHght)
+ E = FreeCAD.Vector(ep[0], ep[1], csHght)
+ C = FreeCAD.Vector(cp[0], cp[1], csHght)
+ scan = (S, E, C, cMode)
+ if scan is False:
+ erFlg = True
+ else:
+ ##if aTyp == 'L':
+ ## stpOvr.append(FreeCAD.Vector(scan[0][0].x, scan[0][0].y, scan[0][0].z))
+ stpOvr.append(scan)
+ if erFlg is False:
+ SCANS.append(stpOvr)
+
+ return SCANS
+
+ # Main planar scan functions
+ def _stepTransitionCmds(self, obj, cutPattern, lstPnt, first, minSTH, tolrnc):
+ cmds = list()
+ rtpd = False
+ horizGC = 'G0'
+ hSpeed = self.horizRapid
+ height = obj.SafeHeight.Value
+
+ if cutPattern in ['Line', 'Circular', 'Spiral']:
+ if obj.OptimizeStepOverTransitions is True:
+ height = minSTH + 2.0
+ # if obj.LayerMode == 'Multi-pass':
+ # rtpd = minSTH
+ elif cutPattern in ['ZigZag', 'CircularZigZag']:
+ if obj.OptimizeStepOverTransitions is True:
+ zChng = first.z - lstPnt.z
+ # PathLog.debug('first.z: {}'.format(first.z))
+ # PathLog.debug('lstPnt.z: {}'.format(lstPnt.z))
+ # PathLog.debug('zChng: {}'.format(zChng))
+ # PathLog.debug('minSTH: {}'.format(minSTH))
+ if abs(zChng) < tolrnc: # transitions to same Z height
+ # PathLog.debug('abs(zChng) < tolrnc')
+ if (minSTH - first.z) > tolrnc:
+ # PathLog.debug('(minSTH - first.z) > tolrnc')
+ height = minSTH + 2.0
+ else:
+ # PathLog.debug('ELSE (minSTH - first.z) > tolrnc')
+ horizGC = 'G1'
+ height = first.z
+ elif (minSTH + (2.0 * tolrnc)) >= max(first.z, lstPnt.z):
+ height = False # allow end of Zig to cut to beginning of Zag
+
+
+ # Create raise, shift, and optional lower commands
+ if height is not False:
+ cmds.append(Path.Command('G0', {'Z': height, 'F': self.vertRapid}))
+ cmds.append(Path.Command(horizGC, {'X': first.x, 'Y': first.y, 'F': hSpeed}))
+ if rtpd is not False: # ReturnToPreviousDepth
+ cmds.append(Path.Command('G0', {'Z': rtpd, 'F': self.vertRapid}))
+
+ return cmds
+
+ def _breakCmds(self, obj, cutPattern, lstPnt, first, minSTH, tolrnc):
+ cmds = list()
+ rtpd = False
+ horizGC = 'G0'
+ hSpeed = self.horizRapid
+ height = obj.SafeHeight.Value
+
+ if cutPattern in ['Line', 'Circular', 'Spiral']:
+ if obj.OptimizeStepOverTransitions is True:
+ height = minSTH + 2.0
+ elif cutPattern in ['ZigZag', 'CircularZigZag']:
+ if obj.OptimizeStepOverTransitions is True:
+ zChng = first.z - lstPnt.z
+ if abs(zChng) < tolrnc: # transitions to same Z height
+ if (minSTH - first.z) > tolrnc:
+ height = minSTH + 2.0
+ else:
+ height = first.z + 2.0 # first.z
+
+ cmds.append(Path.Command('G0', {'Z': height, 'F': self.vertRapid}))
+ cmds.append(Path.Command(horizGC, {'X': first.x, 'Y': first.y, 'F': hSpeed}))
+ if rtpd is not False: # ReturnToPreviousDepth
+ cmds.append(Path.Command('G0', {'Z': rtpd, 'F': self.vertRapid}))
+
+ return cmds
+
+ def _planarGetPDC(self, stl, finalDep, SampleInterval, cutter):
+ pdc = ocl.PathDropCutter() # create a pdc [PathDropCutter] object
+ pdc.setSTL(stl) # add stl model
+ pdc.setCutter(cutter) # add cutter
+ pdc.setZ(finalDep) # set minimumZ (final / target depth value)
+ pdc.setSampling(SampleInterval) # set sampling size
+ return pdc
+
+ # OCL Dropcutter waterline functions
+ def _oclWaterlineOp(self, JOB, obj, mdlIdx, subShp=None):
+ '''_oclWaterlineOp(obj, base) ... Main waterline function to perform waterline extraction from model.'''
+ commands = []
+
+ base = JOB.Model.Group[mdlIdx]
+ bb = self.boundBoxes[mdlIdx]
+ stl = self.modelSTLs[mdlIdx]
+ depOfst = obj.DepthOffset.Value
+
+ # Prepare global holdpoint and layerEndPnt containers
+ if self.holdPoint is None:
+ self.holdPoint = FreeCAD.Vector(0.0, 0.0, 0.0)
+ if self.layerEndPnt is None:
+ self.layerEndPnt = FreeCAD.Vector(0.0, 0.0, 0.0)
+
+ # Set extra offset to diameter of cutter to allow cutter to move around perimeter of model
+ toolDiam = self.cutter.getDiameter()
+
+ if subShp is None:
+ # Get correct boundbox
+ if obj.BoundBox == 'Stock':
+ BS = JOB.Stock
+ bb = BS.Shape.BoundBox
+ elif obj.BoundBox == 'BaseBoundBox':
+ BS = base
+ bb = base.Shape.BoundBox
+
+ xmin = bb.XMin
+ xmax = bb.XMax
+ ymin = bb.YMin
+ ymax = bb.YMax
+ else:
+ xmin = subShp.BoundBox.XMin
+ xmax = subShp.BoundBox.XMax
+ ymin = subShp.BoundBox.YMin
+ ymax = subShp.BoundBox.YMax
+
+ smplInt = obj.SampleInterval.Value
+ minSampInt = 0.001 # value is mm
+ if smplInt < minSampInt:
+ smplInt = minSampInt
+
+ # Determine bounding box length for the OCL scan
+ bbLength = math.fabs(ymax - ymin)
+ numScanLines = int(math.ceil(bbLength / smplInt) + 1) # Number of lines
+
+ # Compute number and size of stepdowns, and final depth
+ if obj.LayerMode == 'Single-pass':
+ depthparams = [obj.FinalDepth.Value]
+ else:
+ depthparams = [dp for dp in self.depthParams]
+ lenDP = len(depthparams)
+
+ # Scan the piece to depth at smplInt
+ oclScan = []
+ oclScan = self._waterlineDropCutScan(stl, smplInt, xmin, xmax, ymin, depthparams[lenDP - 1], numScanLines)
+ oclScan = [FreeCAD.Vector(P.x, P.y, P.z + depOfst) for P in oclScan]
+ lenOS = len(oclScan)
+ ptPrLn = int(lenOS / numScanLines)
+
+ # Convert oclScan list of points to multi-dimensional list
+ scanLines = []
+ for L in range(0, numScanLines):
+ scanLines.append([])
+ for P in range(0, ptPrLn):
+ pi = L * ptPrLn + P
+ scanLines[L].append(oclScan[pi])
+ lenSL = len(scanLines)
+ pntsPerLine = len(scanLines[0])
+ PathLog.debug("--OCL scan: " + str(lenSL * pntsPerLine) + " points, with " + str(numScanLines) + " lines and " + str(pntsPerLine) + " pts/line")
+
+ # Extract Wl layers per depthparams
+ lyr = 0
+ cmds = []
+ layTime = time.time()
+ self.topoMap = []
+ for layDep in depthparams:
+ cmds = self._getWaterline(obj, scanLines, layDep, lyr, lenSL, pntsPerLine)
+ commands.extend(cmds)
+ lyr += 1
+ PathLog.debug("--All layer scans combined took " + str(time.time() - layTime) + " s")
+ return commands
+
+ def _waterlineDropCutScan(self, stl, smplInt, xmin, xmax, ymin, fd, numScanLines):
+ '''_waterlineDropCutScan(stl, smplInt, xmin, xmax, ymin, fd, numScanLines) ...
+ Perform OCL scan for waterline purpose.'''
+ pdc = ocl.PathDropCutter() # create a pdc
+ pdc.setSTL(stl)
+ pdc.setCutter(self.cutter)
+ pdc.setZ(fd) # set minimumZ (final / target depth value)
+ pdc.setSampling(smplInt)
+
+ # Create line object as path
+ path = ocl.Path() # create an empty path object
+ for nSL in range(0, numScanLines):
+ yVal = ymin + (nSL * smplInt)
+ p1 = ocl.Point(xmin, yVal, fd) # start-point of line
+ p2 = ocl.Point(xmax, yVal, fd) # end-point of line
+ path.append(ocl.Line(p1, p2))
+ # path.append(l) # add the line to the path
+ pdc.setPath(path)
+ pdc.run() # run drop-cutter on the path
+
+ # return the list of points
+ return pdc.getCLPoints()
+
+ def _getWaterline(self, obj, scanLines, layDep, lyr, lenSL, pntsPerLine):
+ '''_getWaterline(obj, scanLines, layDep, lyr, lenSL, pntsPerLine) ... Get waterline.'''
+ commands = []
+ cmds = []
+ loopList = []
+ self.topoMap = []
+ # Create topo map from scanLines (highs and lows)
+ self.topoMap = self._createTopoMap(scanLines, layDep, lenSL, pntsPerLine)
+ # Add buffer lines and columns to topo map
+ self._bufferTopoMap(lenSL, pntsPerLine)
+ # Identify layer waterline from OCL scan
+ self._highlightWaterline(4, 9)
+ # Extract waterline and convert to gcode
+ loopList = self._extractWaterlines(obj, scanLines, lyr, layDep)
+ # save commands
+ for loop in loopList:
+ cmds = self._loopToGcode(obj, layDep, loop)
+ commands.extend(cmds)
+ return commands
+
+ def _createTopoMap(self, scanLines, layDep, lenSL, pntsPerLine):
+ '''_createTopoMap(scanLines, layDep, lenSL, pntsPerLine) ... Create topo map version of OCL scan data.'''
+ topoMap = []
+ for L in range(0, lenSL):
+ topoMap.append([])
+ for P in range(0, pntsPerLine):
+ if scanLines[L][P].z > layDep:
+ topoMap[L].append(2)
+ else:
+ topoMap[L].append(0)
+ return topoMap
+
+ def _bufferTopoMap(self, lenSL, pntsPerLine):
+ '''_bufferTopoMap(lenSL, pntsPerLine) ... Add buffer boarder of zeros to all sides to topoMap data.'''
+ pre = [0, 0]
+ post = [0, 0]
+ for p in range(0, pntsPerLine):
+ pre.append(0)
+ post.append(0)
+ for l in range(0, lenSL):
+ self.topoMap[l].insert(0, 0)
+ self.topoMap[l].append(0)
+ self.topoMap.insert(0, pre)
+ self.topoMap.append(post)
+ return True
+
+ def _highlightWaterline(self, extraMaterial, insCorn):
+ '''_highlightWaterline(extraMaterial, insCorn) ... Highlight the waterline data, separating from extra material.'''
+ TM = self.topoMap
+ lastPnt = len(TM[1]) - 1
+ lastLn = len(TM) - 1
+ highFlag = 0
+
+ # ("--Convert parallel data to ridges")
+ for lin in range(1, lastLn):
+ for pt in range(1, lastPnt): # Ignore first and last points
+ if TM[lin][pt] == 0:
+ if TM[lin][pt + 1] == 2: # step up
+ TM[lin][pt] = 1
+ if TM[lin][pt - 1] == 2: # step down
+ TM[lin][pt] = 1
+
+ # ("--Convert perpendicular data to ridges and highlight ridges")
+ for pt in range(1, lastPnt): # Ignore first and last points
+ for lin in range(1, lastLn):
+ if TM[lin][pt] == 0:
+ highFlag = 0
+ if TM[lin + 1][pt] == 2: # step up
+ TM[lin][pt] = 1
+ if TM[lin - 1][pt] == 2: # step down
+ TM[lin][pt] = 1
+ elif TM[lin][pt] == 2:
+ highFlag += 1
+ if highFlag == 3:
+ if TM[lin - 1][pt - 1] < 2 or TM[lin - 1][pt + 1] < 2:
+ highFlag = 2
+ else:
+ TM[lin - 1][pt] = extraMaterial
+ highFlag = 2
+
+ # ("--Square corners")
+ for pt in range(1, lastPnt):
+ for lin in range(1, lastLn):
+ if TM[lin][pt] == 1: # point == 1
+ cont = True
+ if TM[lin + 1][pt] == 0: # forward == 0
+ if TM[lin + 1][pt - 1] == 1: # forward left == 1
+ if TM[lin][pt - 1] == 2: # left == 2
+ TM[lin + 1][pt] = 1 # square the corner
+ cont = False
+
+ if cont is True and TM[lin + 1][pt + 1] == 1: # forward right == 1
+ if TM[lin][pt + 1] == 2: # right == 2
+ TM[lin + 1][pt] = 1 # square the corner
+ cont = True
+
+ if TM[lin - 1][pt] == 0: # back == 0
+ if TM[lin - 1][pt - 1] == 1: # back left == 1
+ if TM[lin][pt - 1] == 2: # left == 2
+ TM[lin - 1][pt] = 1 # square the corner
+ cont = False
+
+ if cont is True and TM[lin - 1][pt + 1] == 1: # back right == 1
+ if TM[lin][pt + 1] == 2: # right == 2
+ TM[lin - 1][pt] = 1 # square the corner
+
+ # remove inside corners
+ for pt in range(1, lastPnt):
+ for lin in range(1, lastLn):
+ if TM[lin][pt] == 1: # point == 1
+ if TM[lin][pt + 1] == 1:
+ if TM[lin - 1][pt + 1] == 1 or TM[lin + 1][pt + 1] == 1:
+ TM[lin][pt + 1] = insCorn
+ elif TM[lin][pt - 1] == 1:
+ if TM[lin - 1][pt - 1] == 1 or TM[lin + 1][pt - 1] == 1:
+ TM[lin][pt - 1] = insCorn
+
+ return True
+
+ def _extractWaterlines(self, obj, oclScan, lyr, layDep):
+ '''_extractWaterlines(obj, oclScan, lyr, layDep) ... Extract water lines from OCL scan data.'''
+ srch = True
+ lastPnt = len(self.topoMap[0]) - 1
+ lastLn = len(self.topoMap) - 1
+ maxSrchs = 5
+ srchCnt = 1
+ loopList = []
+ loop = []
+ loopNum = 0
+
+ if self.CutClimb is True:
+ lC = [-1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0]
+ pC = [-1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1]
+ else:
+ lC = [1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0]
+ pC = [-1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1]
+
+ while srch is True:
+ srch = False
+ if srchCnt > maxSrchs:
+ PathLog.debug("Max search scans, " + str(maxSrchs) + " reached\nPossible incomplete waterline result!")
+ break
+ for L in range(1, lastLn):
+ for P in range(1, lastPnt):
+ if self.topoMap[L][P] == 1:
+ # start loop follow
+ srch = True
+ loopNum += 1
+ loop = self._trackLoop(oclScan, lC, pC, L, P, loopNum)
+ self.topoMap[L][P] = 0 # Mute the starting point
+ loopList.append(loop)
+ srchCnt += 1
+ PathLog.debug("Search count for layer " + str(lyr) + " is " + str(srchCnt) + ", with " + str(loopNum) + " loops.")
+ return loopList
+
+ def _trackLoop(self, oclScan, lC, pC, L, P, loopNum):
+ '''_trackLoop(oclScan, lC, pC, L, P, loopNum) ... Track the loop direction.'''
+ loop = [oclScan[L - 1][P - 1]] # Start loop point list
+ cur = [L, P, 1]
+ prv = [L, P - 1, 1]
+ nxt = [L, P + 1, 1]
+ follow = True
+ ptc = 0
+ ptLmt = 200000
+ while follow is True:
+ ptc += 1
+ if ptc > ptLmt:
+ PathLog.debug("Loop number " + str(loopNum) + " at [" + str(nxt[0]) + ", " + str(nxt[1]) + "] pnt count exceeds, " + str(ptLmt) + ". Stopped following loop.")
+ break
+ nxt = self._findNextWlPoint(lC, pC, cur[0], cur[1], prv[0], prv[1]) # get next point
+ loop.append(oclScan[nxt[0] - 1][nxt[1] - 1]) # add it to loop point list
+ self.topoMap[nxt[0]][nxt[1]] = nxt[2] # Mute the point, if not Y stem
+ if nxt[0] == L and nxt[1] == P: # check if loop complete
+ follow = False
+ elif nxt[0] == cur[0] and nxt[1] == cur[1]: # check if line cannot be detected
+ follow = False
+ prv = cur
+ cur = nxt
+ return loop
+
+ def _findNextWlPoint(self, lC, pC, cl, cp, pl, pp):
+ '''_findNextWlPoint(lC, pC, cl, cp, pl, pp) ...
+ Find the next waterline point in the point cloud layer provided.'''
+ dl = cl - pl
+ dp = cp - pp
+ num = 0
+ i = 3
+ s = 0
+ mtch = 0
+ found = False
+ while mtch < 8: # check all 8 points around current point
+ if lC[i] == dl:
+ if pC[i] == dp:
+ s = i - 3
+ found = True
+ # Check for y branch where current point is connection between branches
+ for y in range(1, mtch):
+ if lC[i + y] == dl:
+ if pC[i + y] == dp:
+ num = 1
+ break
+ break
+ i += 1
+ mtch += 1
+ if found is False:
+ # ("_findNext: No start point found.")
+ return [cl, cp, num]
+
+ for r in range(0, 8):
+ l = cl + lC[s + r]
+ p = cp + pC[s + r]
+ if self.topoMap[l][p] == 1:
+ return [l, p, num]
+
+ # ("_findNext: No next pnt found")
+ return [cl, cp, num]
+
+ def _loopToGcode(self, obj, layDep, loop):
+ '''_loopToGcode(obj, layDep, loop) ... Convert set of loop points to Gcode.'''
+ # generate the path commands
+ output = []
+
+ prev = FreeCAD.Vector(2135984513.165, -58351896873.17455, 13838638431.861)
+ nxt = FreeCAD.Vector(0.0, 0.0, 0.0)
+
+ # Create first point
+ pnt = FreeCAD.Vector(loop[0].x, loop[0].y, layDep)
+
+ # Position cutter to begin loop
+ output.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid}))
+ output.append(Path.Command('G0', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizRapid}))
+ output.append(Path.Command('G1', {'Z': pnt.z, 'F': self.vertFeed}))
+
+ lenCLP = len(loop)
+ lastIdx = lenCLP - 1
+ # Cycle through each point on loop
+ for i in range(0, lenCLP):
+ if i < lastIdx:
+ nxt.x = loop[i + 1].x
+ nxt.y = loop[i + 1].y
+ nxt.z = layDep
+
+ output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizFeed}))
+
+ # Rotate point data
+ prev = pnt
+ pnt = nxt
+
+ # Save layer end point for use in transitioning to next layer
+ self.layerEndPnt = pnt
+
+ return output
+
+ # Experimental waterline functions
+ def _experimentalWaterlineOp(self, JOB, obj, mdlIdx, subShp=None):
+ '''_waterlineOp(JOB, obj, mdlIdx, subShp=None) ...
+ Main waterline function to perform waterline extraction from model.'''
+ PathLog.debug('_experimentalWaterlineOp()')
+
+ commands = []
+ t_begin = time.time()
+ base = JOB.Model.Group[mdlIdx]
+ # bb = self.boundBoxes[mdlIdx]
+ # stl = self.modelSTLs[mdlIdx]
+ # safeSTL = self.safeSTLs[mdlIdx]
+ self.endVector = None
+
+ finDep = obj.FinalDepth.Value + (self.geoTlrnc / 10.0)
+ depthParams = PathUtils.depth_params(obj.ClearanceHeight.Value, obj.SafeHeight.Value, obj.StartDepth.Value, obj.StepDown.Value, 0.0, finDep)
+
+ # Compute number and size of stepdowns, and final depth
+ if obj.LayerMode == 'Single-pass':
+ depthparams = [finDep]
+ else:
+ depthparams = [dp for dp in depthParams]
+ PathLog.debug('Experimental Waterline depthparams:\n{}'.format(depthparams))
+
+ # Prepare PathDropCutter objects with STL data
+ # safePDC = self._planarGetPDC(safeSTL, depthparams[lenDP - 1], obj.SampleInterval.Value, self.cutter)
+
+ buffer = self.cutter.getDiameter() * 10.0
+ borderFace = Part.Face(self._makeExtendedBoundBox(JOB.Stock.Shape.BoundBox, buffer, 0.0))
+
+ # Get correct boundbox
+ if obj.BoundBox == 'Stock':
+ stockEnv = PathSurfaceSupport.getShapeEnvelope(JOB.Stock.Shape)
+ bbFace = PathSurfaceSupport.getCrossSection(stockEnv) # returned at Z=0.0
+ elif obj.BoundBox == 'BaseBoundBox':
+ baseEnv = PathSurfaceSupport.getShapeEnvelope(base.Shape)
+ bbFace = PathSurfaceSupport.getCrossSection(baseEnv) # returned at Z=0.0
+
+ trimFace = borderFace.cut(bbFace)
+ if self.showDebugObjects is True:
+ TF = FreeCAD.ActiveDocument.addObject('Part::Feature', 'trimFace')
+ TF.Shape = trimFace
+ TF.purgeTouched()
+ self.tempGroup.addObject(TF)
+
+ # Cycle through layer depths
+ CUTAREAS = self._getCutAreas(base.Shape, depthparams, bbFace, trimFace, borderFace)
+ if not CUTAREAS:
+ PathLog.error('No cross-section cut areas identified.')
+ return commands
+
+ caCnt = 0
+ ofst = obj.BoundaryAdjustment.Value
+ ofst -= self.radius # (self.radius + (tolrnc / 10.0))
+ caLen = len(CUTAREAS)
+ lastCA = caLen - 1
+ lastClearArea = None
+ lastCsHght = None
+ clearLastLayer = True
+ for ca in range(0, caLen):
+ area = CUTAREAS[ca]
+ csHght = area.BoundBox.ZMin
+ csHght += obj.DepthOffset.Value
+ cont = False
+ caCnt += 1
+ if area.Area > 0.0:
+ cont = True
+ caWireCnt = len(area.Wires) - 1 # first wire is boundFace wire
+ if self.showDebugObjects:
+ CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'cutArea_{}'.format(caCnt))
+ CA.Shape = area
+ CA.purgeTouched()
+ self.tempGroup.addObject(CA)
+ else:
+ data = FreeCAD.Units.Quantity(csHght, FreeCAD.Units.Length).UserString
+ PathLog.debug('Cut area at {} is zero.'.format(data))
+
+ # get offset wire(s) based upon cross-section cut area
+ if cont:
+ area.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - area.BoundBox.ZMin))
+ activeArea = area.cut(trimFace)
+ activeAreaWireCnt = len(activeArea.Wires) # first wire is boundFace wire
+ if self.showDebugObjects:
+ CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'activeArea_{}'.format(caCnt))
+ CA.Shape = activeArea
+ CA.purgeTouched()
+ self.tempGroup.addObject(CA)
+ ofstArea = PathSurfaceSupport.extractFaceOffset(activeArea, ofst, self.wpc, makeComp=False)
+ if not ofstArea:
+ data = FreeCAD.Units.Quantity(csHght, FreeCAD.Units.Length).UserString
+ PathLog.debug('No offset area returned for cut area depth at {}.'.format(data))
+ cont = False
+
+ if cont:
+ # Identify solid areas in the offset data
+ ofstSolidFacesList = self._getSolidAreasFromPlanarFaces(ofstArea)
+ if ofstSolidFacesList:
+ clearArea = Part.makeCompound(ofstSolidFacesList)
+ if self.showDebugObjects is True:
+ CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'clearArea_{}'.format(caCnt))
+ CA.Shape = clearArea
+ CA.purgeTouched()
+ self.tempGroup.addObject(CA)
+ else:
+ cont = False
+ data = FreeCAD.Units.Quantity(csHght, FreeCAD.Units.Length).UserString
+ PathLog.error('Could not determine solid faces at {}.'.format(data))
+
+ if cont:
+ # Make waterline path for current CUTAREA depth (csHght)
+ commands.extend(self._wiresToWaterlinePath(obj, clearArea, csHght))
+ clearArea.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - clearArea.BoundBox.ZMin))
+ lastClearArea = clearArea
+ lastCsHght = csHght
+
+ # Clear layer as needed
+ (clrLyr, clearLastLayer) = self._clearLayer(obj, ca, lastCA, clearLastLayer)
+ if clrLyr == 'Offset':
+ commands.extend(self._makeOffsetLayerPaths(obj, clearArea, csHght))
+ elif clrLyr:
+ cutPattern = obj.CutPattern
+ if clearLastLayer is False:
+ cutPattern = obj.ClearLastLayer
+ commands.extend(self._makeCutPatternLayerPaths(JOB, obj, clearArea, csHght, cutPattern))
+ # Efor
+
+ if clearLastLayer:
+ (clrLyr, cLL) = self._clearLayer(obj, 1, 1, False)
+ lastClearArea.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - lastClearArea.BoundBox.ZMin))
+ if clrLyr == 'Offset':
+ commands.extend(self._makeOffsetLayerPaths(obj, lastClearArea, lastCsHght))
+ elif clrLyr:
+ commands.extend(self._makeCutPatternLayerPaths(JOB, obj, lastClearArea, lastCsHght, obj.ClearLastLayer))
+
+ PathLog.info("Waterline: All layer scans combined took " + str(time.time() - t_begin) + " s")
+ return commands
+
+ def _getCutAreas(self, shape, depthparams, bbFace, trimFace, borderFace):
+ '''_getCutAreas(JOB, shape, depthparams, bbFace, borderFace) ...
+ Takes shape, depthparams and base-envelope-cross-section, and
+ returns a list of cut areas - one for each depth.'''
+ PathLog.debug('_getCutAreas()')
+
+ CUTAREAS = list()
+ isFirst = True
+ lenDP = len(depthparams)
+
+ # Cycle through layer depths
+ for dp in range(0, lenDP):
+ csHght = depthparams[dp]
+ # PathLog.debug('Depth {} is {}'.format(dp + 1, csHght))
+
+ # Get slice at depth of shape
+ csFaces = self._getModelCrossSection(shape, csHght) # returned at Z=0.0
+ if not csFaces:
+ data = FreeCAD.Units.Quantity(csHght, FreeCAD.Units.Length).UserString
+ else:
+ if len(csFaces) > 0:
+ useFaces = self._getSolidAreasFromPlanarFaces(csFaces)
+ else:
+ useFaces = False
+
+ if useFaces:
+ compAdjFaces = Part.makeCompound(useFaces)
+
+ if self.showDebugObjects is True:
+ CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpSolids_{}'.format(dp + 1))
+ CA.Shape = compAdjFaces
+ CA.purgeTouched()
+ self.tempGroup.addObject(CA)
+
+ if isFirst:
+ allPrevComp = compAdjFaces
+ cutArea = borderFace.cut(compAdjFaces)
+ else:
+ preCutArea = borderFace.cut(compAdjFaces)
+ cutArea = preCutArea.cut(allPrevComp) # cut out higher layers to avoid cutting recessed areas
+ allPrevComp = allPrevComp.fuse(compAdjFaces)
+ cutArea.translate(FreeCAD.Vector(0.0, 0.0, csHght - cutArea.BoundBox.ZMin))
+ CUTAREAS.append(cutArea)
+ isFirst = False
+ else:
+ PathLog.error('No waterline at depth: {} mm.'.format(csHght))
+ # Efor
+
+ if len(CUTAREAS) > 0:
+ return CUTAREAS
+
+ return False
+
+ def _wiresToWaterlinePath(self, obj, ofstPlnrShp, csHght):
+ PathLog.debug('_wiresToWaterlinePath()')
+ commands = list()
+
+ # Translate path geometry to layer height
+ ofstPlnrShp.translate(FreeCAD.Vector(0.0, 0.0, csHght - ofstPlnrShp.BoundBox.ZMin))
+ if self.showDebugObjects is True:
+ OA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'waterlinePathArea_{}'.format(round(csHght, 2)))
+ OA.Shape = ofstPlnrShp
+ OA.purgeTouched()
+ self.tempGroup.addObject(OA)
+
+ commands.append(Path.Command('N (Cut Area {}.)'.format(round(csHght, 2))))
+ start = 1
+ if csHght < obj.IgnoreOuterAbove:
+ start = 0
+ for w in range(start, len(ofstPlnrShp.Wires)):
+ wire = ofstPlnrShp.Wires[w]
+ V = wire.Vertexes
+ if obj.CutMode == 'Climb':
+ lv = len(V) - 1
+ startVect = FreeCAD.Vector(V[lv].X, V[lv].Y, V[lv].Z)
+ else:
+ startVect = FreeCAD.Vector(V[0].X, V[0].Y, V[0].Z)
+
+ commands.append(Path.Command('N (Wire {}.)'.format(w)))
+ (cmds, endVect) = self._wireToPath(obj, wire, startVect)
+ commands.extend(cmds)
+ commands.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
+
+ return commands
+
+ def _makeCutPatternLayerPaths(self, JOB, obj, clrAreaShp, csHght, cutPattern):
+ PathLog.debug('_makeCutPatternLayerPaths()')
+ commands = []
+
+ clrAreaShp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - clrAreaShp.BoundBox.ZMin))
+
+ # Convert pathGeom to gcode more efficiently
+ if cutPattern == 'Offset':
+ commands.extend(self._makeOffsetLayerPaths(obj, clrAreaShp, csHght))
+ else:
+ # Request path geometry from external support class
+ PGG = PathSurfaceSupport.PathGeometryGenerator(obj, clrAreaShp, cutPattern)
+ if self.showDebugObjects:
+ PGG.setDebugObjectsGroup(self.tempGroup)
+ self.tmpCOM = PGG.getCenterOfPattern()
+ pathGeom = PGG.generatePathGeometry()
+ if not pathGeom:
+ PathLog.warning('No path geometry generated.')
+ return commands
+ pathGeom.translate(FreeCAD.Vector(0.0, 0.0, csHght - pathGeom.BoundBox.ZMin))
+
+ if self.showDebugObjects is True:
+ OA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'pathGeom_{}'.format(round(csHght, 2)))
+ OA.Shape = pathGeom
+ OA.purgeTouched()
+ self.tempGroup.addObject(OA)
+
+ if cutPattern == 'Line':
+ pntSet = PathSurfaceSupport.pathGeomToLinesPointSet(obj, pathGeom, self.CutClimb, self.toolDiam, self.closedGap, self.gaps)
+ elif cutPattern == 'ZigZag':
+ pntSet = PathSurfaceSupport.pathGeomToZigzagPointSet(obj, pathGeom, self.CutClimb, self.toolDiam, self.closedGap, self.gaps)
+ elif cutPattern in ['Circular', 'CircularZigZag']:
+ pntSet = PathSurfaceSupport.pathGeomToCircularPointSet(obj, pathGeom, self.CutClimb, self.toolDiam, self.closedGap, self.gaps, self.tmpCOM)
+ elif cutPattern == 'Spiral':
+ pntSet = PathSurfaceSupport.pathGeomToSpiralPointSet(obj, pathGeom)
+
+ stpOVRS = self._getExperimentalWaterlinePaths(pntSet, csHght, cutPattern)
+ safePDC = False
+ cmds = self._clearGeomToPaths(JOB, obj, safePDC, stpOVRS, cutPattern)
+ commands.extend(cmds)
+
+ return commands
+
+ def _makeOffsetLayerPaths(self, obj, clrAreaShp, csHght):
+ PathLog.debug('_makeOffsetLayerPaths()')
+ cmds = list()
+ ofst = 0.0 - self.cutOut
+ shape = clrAreaShp
+ cont = True
+ cnt = 0
+ while cont:
+ ofstArea = PathSurfaceSupport.extractFaceOffset(shape, ofst, self.wpc, makeComp=True)
+ if not ofstArea:
+ break
+ for F in ofstArea.Faces:
+ cmds.extend(self._wiresToWaterlinePath(obj, F, csHght))
+ shape = ofstArea
+ if cnt == 0:
+ ofst = 0.0 - self.cutOut
+ cnt += 1
+ return cmds
+
+ def _clearGeomToPaths(self, JOB, obj, safePDC, stpOVRS, cutPattern):
+ PathLog.debug('_clearGeomToPaths()')
+
+ GCODE = [Path.Command('N (Beginning of Single-pass layer.)', {})]
+ tolrnc = JOB.GeometryTolerance.Value
+ lenstpOVRS = len(stpOVRS)
+ lstSO = lenstpOVRS - 1
+ lstStpOvr = False
+ gDIR = ['G3', 'G2']
+
+ if self.CutClimb is True:
+ gDIR = ['G2', 'G3']
+
+ # Send cutter to x,y position of first point on first line
+ first = stpOVRS[0][0][0] # [step][item][point]
+ GCODE.append(Path.Command('G0', {'X': first.x, 'Y': first.y, 'F': self.horizRapid}))
+
+ # Cycle through step-over sections (line segments or arcs)
+ odd = True
+ lstStpEnd = None
+ for so in range(0, lenstpOVRS):
+ cmds = list()
+ PRTS = stpOVRS[so]
+ lenPRTS = len(PRTS)
+ first = PRTS[0][0] # first point of arc/line stepover group
+ last = None
+ cmds.append(Path.Command('N (Begin step {}.)'.format(so), {}))
+ if so == lstSO:
+ lstStpOvr = True
+
+ if so > 0:
+ if cutPattern == 'CircularZigZag':
+ if odd:
+ odd = False
+ else:
+ odd = True
+ # minTrnsHght = self._getMinSafeTravelHeight(safePDC, lstStpEnd, first) # Check safe travel height against fullSTL
+ minTrnsHght = obj.SafeHeight.Value
+ # cmds.append(Path.Command('N (Transition: last, first: {}, {}: minSTH: {})'.format(lstStpEnd, first, minTrnsHght), {}))
+ cmds.extend(self._stepTransitionCmds(obj, cutPattern, lstStpEnd, first, minTrnsHght, tolrnc))
+
+ # Cycle through current step-over parts
+ for i in range(0, lenPRTS):
+ prt = PRTS[i]
+ # PathLog.debug('prt: {}'.format(prt))
+ if prt == 'BRK':
+ nxtStart = PRTS[i + 1][0]
+ # minSTH = self._getMinSafeTravelHeight(safePDC, last, nxtStart) # Check safe travel height against fullSTL
+ minSTH = obj.SafeHeight.Value
+ cmds.append(Path.Command('N (Break)', {}))
+ cmds.extend(self._breakCmds(obj, cutPattern, last, nxtStart, minSTH, tolrnc))
+ else:
+ cmds.append(Path.Command('N (part {}.)'.format(i + 1), {}))
+ if cutPattern in ['Line', 'ZigZag', 'Spiral']:
+ start, last = prt
+ cmds.append(Path.Command('G1', {'X': start.x, 'Y': start.y, 'Z': start.z, 'F': self.horizFeed}))
+ cmds.append(Path.Command('G1', {'X': last.x, 'Y': last.y, 'F': self.horizFeed}))
+ elif cutPattern in ['Circular', 'CircularZigZag']:
+ # isCircle = True if lenPRTS == 1 else False
+ isZigZag = True if cutPattern == 'CircularZigZag' else False
+ PathLog.debug('so, isZigZag, odd, cMode: {}, {}, {}, {}'.format(so, isZigZag, odd, prt[3]))
+ gcode = self._makeGcodeArc(prt, gDIR, odd, isZigZag)
+ cmds.extend(gcode)
+ cmds.append(Path.Command('N (End of step {}.)'.format(so), {}))
+ GCODE.extend(cmds) # save line commands
+ lstStpEnd = last
+ # Efor
+
+ # Raise to safe height after clearing
+ GCODE.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
+
+ return GCODE
+
+ def _getSolidAreasFromPlanarFaces(self, csFaces):
+ PathLog.debug('_getSolidAreasFromPlanarFaces()')
+ holds = list()
+ useFaces = list()
+ lenCsF = len(csFaces)
+ PathLog.debug('lenCsF: {}'.format(lenCsF))
+
+ if lenCsF == 1:
+ useFaces = csFaces
+ else:
+ fIds = list()
+ aIds = list()
+ pIds = list()
+ cIds = list()
+
+ for af in range(0, lenCsF):
+ fIds.append(af) # face ids
+ aIds.append(af) # face ids
+ pIds.append(-1) # parent ids
+ cIds.append(False) # cut ids
+ holds.append(False)
+
+ while len(fIds) > 0:
+ li = fIds.pop()
+ low = csFaces[li] # senior face
+ pIds = self._idInternalFeature(csFaces, fIds, pIds, li, low)
+
+ for af in range(lenCsF - 1, -1, -1): # cycle from last item toward first
+ prnt = pIds[af]
+ if prnt == -1:
+ stack = -1
+ else:
+ stack = [af]
+ # get_face_ids_to_parent
+ stack.insert(0, prnt)
+ nxtPrnt = pIds[prnt]
+ # find af value for nxtPrnt
+ while nxtPrnt != -1:
+ stack.insert(0, nxtPrnt)
+ nxtPrnt = pIds[nxtPrnt]
+ cIds[af] = stack
+
+ for af in range(0, lenCsF):
+ pFc = cIds[af]
+ if pFc == -1:
+ # Simple, independent region
+ holds[af] = csFaces[af] # place face in hold
+ else:
+ # Compound region
+ cnt = len(pFc)
+ if cnt % 2.0 == 0.0:
+ # even is donut cut
+ inr = pFc[cnt - 1]
+ otr = pFc[cnt - 2]
+ holds[otr] = holds[otr].cut(csFaces[inr])
+ else:
+ # odd is floating solid
+ holds[af] = csFaces[af]
+
+ for af in range(0, lenCsF):
+ if holds[af]:
+ useFaces.append(holds[af]) # save independent solid
+ # Eif
+
+ if len(useFaces) > 0:
+ return useFaces
+
+ return False
+
+ def _getModelCrossSection(self, shape, csHght):
+ PathLog.debug('getCrossSection()')
+ wires = list()
+
+ def byArea(fc):
+ return fc.Area
+
+ for i in shape.slice(FreeCAD.Vector(0, 0, 1), csHght):
+ wires.append(i)
+
+ if len(wires) > 0:
+ for w in wires:
+ if w.isClosed() is False:
+ return False
+ FCS = list()
+ for w in wires:
+ w.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - w.BoundBox.ZMin))
+ FCS.append(Part.Face(w))
+ FCS.sort(key=byArea, reverse=True)
+ return FCS
+ else:
+ PathLog.debug(' -No wires from .slice() method')
+
+ return False
+
+ def _isInBoundBox(self, outShp, inShp):
+ obb = outShp.BoundBox
+ ibb = inShp.BoundBox
+
+ if obb.XMin < ibb.XMin:
+ if obb.XMax > ibb.XMax:
+ if obb.YMin < ibb.YMin:
+ if obb.YMax > ibb.YMax:
+ return True
+ return False
+
+ def _idInternalFeature(self, csFaces, fIds, pIds, li, low):
+ Ids = list()
+ for i in fIds:
+ Ids.append(i)
+ while len(Ids) > 0:
+ hi = Ids.pop()
+ high = csFaces[hi]
+ if self._isInBoundBox(high, low):
+ cmn = high.common(low)
+ if cmn.Area > 0.0:
+ pIds[li] = hi
+ break
+
+ return pIds
+
+ def _wireToPath(self, obj, wire, startVect):
+ '''_wireToPath(obj, wire, startVect) ... wire to path.'''
+ PathLog.track()
+
+ paths = []
+ pathParams = {} # pylint: disable=assignment-from-no-return
+
+ pathParams['shapes'] = [wire]
+ pathParams['feedrate'] = self.horizFeed
+ pathParams['feedrate_v'] = self.vertFeed
+ pathParams['verbose'] = True
+ pathParams['resume_height'] = obj.SafeHeight.Value
+ pathParams['retraction'] = obj.ClearanceHeight.Value
+ pathParams['return_end'] = True
+ # Note that emitting preambles between moves breaks some dressups and prevents path optimization on some controllers
+ pathParams['preamble'] = False
+ pathParams['start'] = startVect
+
+ (pp, end_vector) = Path.fromShapes(**pathParams)
+ paths.extend(pp.Commands)
+
+ self.endVector = end_vector # pylint: disable=attribute-defined-outside-init
+
+ return (paths, end_vector)
+
+ def _makeExtendedBoundBox(self, wBB, bbBfr, zDep):
+ pl = FreeCAD.Placement()
+ pl.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 0)
+ pl.Base = FreeCAD.Vector(0, 0, 0)
+
+ p1 = FreeCAD.Vector(wBB.XMin - bbBfr, wBB.YMin - bbBfr, zDep)
+ p2 = FreeCAD.Vector(wBB.XMax + bbBfr, wBB.YMin - bbBfr, zDep)
+ p3 = FreeCAD.Vector(wBB.XMax + bbBfr, wBB.YMax + bbBfr, zDep)
+ p4 = FreeCAD.Vector(wBB.XMin - bbBfr, wBB.YMax + bbBfr, zDep)
+ bb = Part.makePolygon([p1, p2, p3, p4, p1])
+
+ return bb
+
+ def _makeGcodeArc(self, prt, gDIR, odd, isZigZag):
+ cmds = list()
+ strtPnt, endPnt, cntrPnt, cMode = prt
+ gdi = 0
+ if odd:
+ gdi = 1
+ else:
+ if not cMode and isZigZag:
+ gdi = 1
+ gCmd = gDIR[gdi]
+
+ # ijk = self.tmpCOM - strtPnt
+ # ijk = self.tmpCOM.sub(strtPnt) # vector from start to center
+ ijk = cntrPnt.sub(strtPnt) # vector from start to center
+ xyz = endPnt
+ cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed}))
+ cmds.append(Path.Command(gCmd, {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z,
+ 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height
+ 'F': self.horizFeed}))
+ cmds.append(Path.Command('G1', {'X': endPnt.x, 'Y': endPnt.y, 'Z': endPnt.z, 'F': self.horizFeed}))
+
+ return cmds
+
+ def _clearLayer(self, obj, ca, lastCA, clearLastLayer):
+ PathLog.debug('_clearLayer()')
+ clrLyr = False
+
+ if obj.ClearLastLayer == 'Off':
+ if obj.CutPattern != 'None':
+ clrLyr = obj.CutPattern
+ else:
+ obj.CutPattern = 'None'
+ if ca == lastCA: # if current iteration is last layer
+ PathLog.debug('... Clearing bottom layer.')
+ clrLyr = obj.ClearLastLayer
+ clearLastLayer = False
+
+ return (clrLyr, clearLastLayer)
+
+ # Support methods
+ def resetOpVariables(self, all=True):
+ '''resetOpVariables() ... Reset class variables used for instance of operation.'''
+ self.holdPoint = None
+ self.layerEndPnt = None
+ self.onHold = False
+ self.SafeHeightOffset = 2.0
+ self.ClearHeightOffset = 4.0
+ self.layerEndzMax = 0.0
+ self.resetTolerance = 0.0
+ self.holdPntCnt = 0
+ self.bbRadius = 0.0
+ self.axialFeed = 0.0
+ self.axialRapid = 0.0
+ self.FinalDepth = 0.0
+ self.clearHeight = 0.0
+ self.safeHeight = 0.0
+ self.faceZMax = -999999999999.0
+ if all is True:
+ self.cutter = None
+ self.stl = None
+ self.fullSTL = None
+ self.cutOut = 0.0
+ self.radius = 0.0
+ self.useTiltCutter = False
+ return True
+
+ def deleteOpVariables(self, all=True):
+ '''deleteOpVariables() ... Reset class variables used for instance of operation.'''
+ del self.holdPoint
+ del self.layerEndPnt
+ del self.onHold
+ del self.SafeHeightOffset
+ del self.ClearHeightOffset
+ del self.layerEndzMax
+ del self.resetTolerance
+ del self.holdPntCnt
+ del self.bbRadius
+ del self.axialFeed
+ del self.axialRapid
+ del self.FinalDepth
+ del self.clearHeight
+ del self.safeHeight
+ del self.faceZMax
+ if all is True:
+ del self.cutter
+ del self.stl
+ del self.fullSTL
+ del self.cutOut
+ del self.radius
+ del self.useTiltCutter
+ return True
+
+ def setOclCutter(self, obj, safe=False):
+ ''' setOclCutter(obj) ... Translation function to convert FreeCAD tool definition to OCL formatted tool. '''
+ # Set cutter details
+ # https://www.freecadweb.org/api/dd/dfe/classPath_1_1Tool.html#details
+ diam_1 = float(obj.ToolController.Tool.Diameter)
+ lenOfst = obj.ToolController.Tool.LengthOffset if hasattr(obj.ToolController.Tool, 'LengthOffset') else 0
+ FR = obj.ToolController.Tool.FlatRadius if hasattr(obj.ToolController.Tool, 'FlatRadius') else 0
+ CEH = obj.ToolController.Tool.CuttingEdgeHeight if hasattr(obj.ToolController.Tool, 'CuttingEdgeHeight') else 0
+ CEA = obj.ToolController.Tool.CuttingEdgeAngle if hasattr(obj.ToolController.Tool, 'CuttingEdgeAngle') else 0
+
+ # Make safeCutter with 2 mm buffer around physical cutter
+ if safe is True:
+ diam_1 += 4.0
+ if FR != 0.0:
+ FR += 2.0
+
+ PathLog.debug('ToolType: {}'.format(obj.ToolController.Tool.ToolType))
+ if obj.ToolController.Tool.ToolType == 'EndMill':
+ # Standard End Mill
+ return ocl.CylCutter(diam_1, (CEH + lenOfst))
+
+ elif obj.ToolController.Tool.ToolType == 'BallEndMill' and FR == 0.0:
+ # Standard Ball End Mill
+ # OCL -> BallCutter::BallCutter(diameter, length)
+ self.useTiltCutter = True
+ return ocl.BallCutter(diam_1, (diam_1 / 2 + lenOfst))
+
+ elif obj.ToolController.Tool.ToolType == 'BallEndMill' and FR > 0.0:
+ # Bull Nose or Corner Radius cutter
+ # Reference: https://www.fine-tools.com/halbstabfraeser.html
+ # OCL -> BallCutter::BallCutter(diameter, length)
+ return ocl.BullCutter(diam_1, FR, (CEH + lenOfst))
+
+ elif obj.ToolController.Tool.ToolType == 'Engraver' and FR > 0.0:
+ # Bull Nose or Corner Radius cutter
+ # Reference: https://www.fine-tools.com/halbstabfraeser.html
+ # OCL -> ConeCutter::ConeCutter(diameter, angle, lengthOffset)
+ return ocl.ConeCutter(diam_1, (CEA / 2), lenOfst)
+
+ elif obj.ToolController.Tool.ToolType == 'ChamferMill':
+ # Bull Nose or Corner Radius cutter
+ # Reference: https://www.fine-tools.com/halbstabfraeser.html
+ # OCL -> ConeCutter::ConeCutter(diameter, angle, lengthOffset)
+ return ocl.ConeCutter(diam_1, (CEA / 2), lenOfst)
+ else:
+ # Default to standard end mill
+ PathLog.warning("Defaulting cutter to standard end mill.")
+ return ocl.CylCutter(diam_1, (CEH + lenOfst))
+
+
+def SetupProperties():
+ ''' SetupProperties() ... Return list of properties required for operation.'''
+ setup = ['Algorithm', 'AvoidLastX_Faces', 'AvoidLastX_InternalFeatures', 'BoundBox']
+ setup.extend(['BoundaryAdjustment', 'PatternCenterAt', 'PatternCenterCustom'])
+ setup.extend(['ClearLastLayer', 'InternalFeaturesCut', 'InternalFeaturesAdjustment'])
+ setup.extend(['CutMode', 'CutPattern', 'CutPatternAngle', 'CutPatternReversed'])
+ setup.extend(['DepthOffset', 'GapSizes', 'GapThreshold', 'StepOver'])
+ setup.extend(['HandleMultipleFeatures', 'LayerMode', 'OptimizeStepOverTransitions'])
+ setup.extend(['BoundaryEnforcement', 'SampleInterval', 'StartPoint', 'IgnoreOuterAbove'])
+ setup.extend(['UseStartPoint', 'AngularDeflection', 'LinearDeflection', 'ShowTempObjects'])
+ return setup
+
+
+def Create(name, obj=None):
+ '''Create(name) ... Creates and returns a Waterline operation.'''
+ if obj is None:
+ obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
+ obj.Proxy = ObjectWaterline(obj, name)
+ return obj
diff --git a/src/Mod/Path/PathScripts/PathWaterlineGui.py b/src/Mod/Path/PathScripts/PathWaterlineGui.py
index eed15fc3d3..0616bbe6d2 100644
--- a/src/Mod/Path/PathScripts/PathWaterlineGui.py
+++ b/src/Mod/Path/PathScripts/PathWaterlineGui.py
@@ -90,6 +90,8 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
else:
self.form.optimizeEnabled.setCheckState(QtCore.Qt.Unchecked)
+ self.updateVisibility()
+
def getSignalsForUpdate(self, obj):
'''getSignalsForUpdate(obj) ... return list of signals for updating obj'''
signals = []
@@ -106,21 +108,32 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
return signals
def updateVisibility(self):
- if self.form.algorithmSelect.currentText() == 'OCL Dropcutter':
- self.form.cutPattern.setEnabled(False)
- self.form.boundaryAdjustment.setEnabled(False)
- self.form.stepOver.setEnabled(False)
- self.form.sampleInterval.setEnabled(True)
- self.form.optimizeEnabled.setEnabled(True)
- else:
- self.form.cutPattern.setEnabled(True)
- self.form.boundaryAdjustment.setEnabled(True)
+ '''updateVisibility()... Updates visibility of Tasks panel objects.'''
+ Algorithm = self.form.algorithmSelect.currentText()
+ self.form.optimizeEnabled.hide() # Has no independent QLabel object
+
+ if Algorithm == 'OCL Dropcutter':
+ self.form.cutPattern.hide()
+ self.form.cutPattern_label.hide()
+ self.form.boundaryAdjustment.hide()
+ self.form.boundaryAdjustment_label.hide()
+ self.form.stepOver.hide()
+ self.form.stepOver_label.hide()
+ self.form.sampleInterval.show()
+ self.form.sampleInterval_label.show()
+ elif Algorithm == 'Experimental':
+ self.form.cutPattern.show()
+ self.form.boundaryAdjustment.show()
+ self.form.cutPattern_label.show()
+ self.form.boundaryAdjustment_label.show()
if self.form.cutPattern.currentText() == 'None':
- self.form.stepOver.setEnabled(False)
+ self.form.stepOver.hide()
+ self.form.stepOver_label.hide()
else:
- self.form.stepOver.setEnabled(True)
- self.form.sampleInterval.setEnabled(False)
- self.form.optimizeEnabled.setEnabled(False)
+ self.form.stepOver.show()
+ self.form.stepOver_label.show()
+ self.form.sampleInterval.hide()
+ self.form.sampleInterval_label.hide()
def registerSignalHandlers(self, obj):
self.form.algorithmSelect.currentIndexChanged.connect(self.updateVisibility)
diff --git a/src/Mod/TechDraw/Gui/CommandAnnotate.cpp b/src/Mod/TechDraw/Gui/CommandAnnotate.cpp
index 702f4fd9dc..d36a178fc4 100644
--- a/src/Mod/TechDraw/Gui/CommandAnnotate.cpp
+++ b/src/Mod/TechDraw/Gui/CommandAnnotate.cpp
@@ -581,13 +581,13 @@ void CmdTechDrawCenterLineGroup::activated(int iMsg)
Gui::ActionGroup* pcAction = qobject_cast(_pcAction);
pcAction->setIcon(pcAction->actions().at(iMsg)->icon());
switch(iMsg) {
- case 0:
+ case 0: //faces
execCenterLine(this);
break;
- case 1:
+ case 1: //2 lines
exec2LineCenterLine(this);
break;
- case 2:
+ case 2: //2 points
exec2PointCenterLine(this);
break;
default:
@@ -743,29 +743,23 @@ void execCenterLine(Gui::Command* cmd)
if (!faceNames.empty()) {
Gui::Control().showDialog(new TaskDlgCenterLine(baseFeat,
page,
- faceNames));
+ faceNames,
+ false));
} else if (edgeNames.empty()) {
QMessageBox::warning(Gui::getMainWindow(), QObject::tr("Wrong Selection"),
QObject::tr("No CenterLine in selection."));
return;
} else {
- std::string edgeName = edgeNames.front();
- int geomIdx = DrawUtil::getIndexFromName(edgeName);
- const std::vector &geoms = baseFeat->getEdgeGeometry();
- BaseGeom* bg = geoms.at(geomIdx);
-// int clIdx = bg->sourceIndex();
-// TechDraw::CenterLine* cl = baseFeat->getCenterLineByIndex(clIdx);
- std::string tag = bg->getCosmeticTag();
- TechDraw::CenterLine* cl = baseFeat->getCenterLine(tag);
+ TechDraw::CenterLine* cl = baseFeat->getCenterLineBySelection(edgeNames.front());
if (cl == nullptr) {
QMessageBox::warning(Gui::getMainWindow(), QObject::tr("Wrong Selection"),
- QObject::tr("No CenterLine in selection."));
+ QObject::tr("Selection is not a CenterLine."));
return;
}
-
Gui::Control().showDialog(new TaskDlgCenterLine(baseFeat,
page,
- edgeNames.front()));
+ edgeNames.front(),
+ true));
}
}
@@ -825,25 +819,19 @@ void exec2LineCenterLine(Gui::Command* cmd)
if (selectedEdges.size() == 2) {
Gui::Control().showDialog(new TaskDlgCenterLine(dvp,
page,
- selectedEdges));
+ selectedEdges,
+ false));
} else if (selectedEdges.size() == 1) {
- std::string edgeName = selectedEdges.front();
- int geomIdx = DrawUtil::getIndexFromName(edgeName);
- const std::vector &geoms = dvp->getEdgeGeometry();
- BaseGeom* bg = geoms.at(geomIdx);
-// int clIdx = bg->sourceIndex();
-// TechDraw::CenterLine* cl = dvp->getCenterLineByIndex(clIdx);
- std::string tag = bg->getCosmeticTag();
- TechDraw::CenterLine* cl = dvp->getCenterLine(tag);
+ TechDraw::CenterLine* cl = dvp->getCenterLineBySelection(selectedEdges.front());
if (cl == nullptr) {
QMessageBox::warning(Gui::getMainWindow(), QObject::tr("Wrong Selection"),
- QObject::tr("No CenterLine in selection."));
+ QObject::tr("Selection is not a CenterLine."));
return;
} else {
-// Base::Console().Message("CMD::2LineCenter - show edit dialog here\n");
Gui::Control().showDialog(new TaskDlgCenterLine(dvp,
page,
- selectedEdges.front()));
+ selectedEdges.front(),
+ true));
}
} else { //not create, not edit, what is this???
QMessageBox::warning(Gui::getMainWindow(), QObject::tr("Wrong Selection"),
@@ -942,14 +930,23 @@ void exec2PointCenterLine(Gui::Command* cmd)
if (!vertexNames.empty() && (vertexNames.size() == 2)) {
Gui::Control().showDialog(new TaskDlgCenterLine(baseFeat,
page,
- vertexNames));
+ vertexNames,
+ false));
} else if (!edgeNames.empty() && (edgeNames.size() == 1)) {
+ TechDraw::CenterLine* cl = baseFeat->getCenterLineBySelection(edgeNames.front());
+ if (cl == nullptr) {
+ QMessageBox::warning(Gui::getMainWindow(), QObject::tr("Wrong Selection"),
+ QObject::tr("Selection is not a CenterLine."));
+ return;
+ }
+
Gui::Control().showDialog(new TaskDlgCenterLine(baseFeat,
page,
- edgeNames.front()));
+ edgeNames.front(),
+ false));
} else if (vertexNames.empty()) {
QMessageBox::warning(Gui::getMainWindow(), QObject::tr("Wrong Selection"),
- QObject::tr("No CenterLine in selection."));
+ QObject::tr("Need 2 Vertices or 1 CenterLine."));
return;
}
}
diff --git a/src/Mod/TechDraw/Gui/TaskCenterLine.cpp b/src/Mod/TechDraw/Gui/TaskCenterLine.cpp
index 79d110eca2..f6701cfa86 100644
--- a/src/Mod/TechDraw/Gui/TaskCenterLine.cpp
+++ b/src/Mod/TechDraw/Gui/TaskCenterLine.cpp
@@ -74,15 +74,16 @@ using namespace TechDrawGui;
//ctor for edit
TaskCenterLine::TaskCenterLine(TechDraw::DrawViewPart* partFeat,
TechDraw::DrawPage* page,
- std::string edgeName) :
+ std::string edgeName,
+ bool editMode) :
ui(new Ui_TaskCenterLine),
m_partFeat(partFeat),
m_basePage(page),
m_createMode(false),
m_edgeName(edgeName),
m_type(0), //0 - Face, 1 - 2 Lines, 2 - 2 points
- m_mode(0) //0 - vertical, 1 - horizontal, 2 - aligned
-
+ m_mode(0), //0 - vertical, 1 - horizontal, 2 - aligned
+ m_editMode(editMode)
{
// Base::Console().Message("TCL::TCL() - edit mode\n");
ui->setupUi(this);
@@ -104,14 +105,16 @@ TaskCenterLine::TaskCenterLine(TechDraw::DrawViewPart* partFeat,
//ctor for creation
TaskCenterLine::TaskCenterLine(TechDraw::DrawViewPart* partFeat,
TechDraw::DrawPage* page,
- std::vector subNames) :
+ std::vector subNames,
+ bool editMode) :
ui(new Ui_TaskCenterLine),
m_partFeat(partFeat),
m_basePage(page),
m_createMode(true),
m_subNames(subNames),
m_type(0), //0 - Face, 1 - 2 Lines, 2 - 2 points
- m_mode(0) //0 - vertical, 1 - horizontal, 2 - aligned
+ m_mode(0), //0 - vertical, 1 - horizontal, 2 - aligned
+ m_editMode(editMode)
{
// Base::Console().Message("TCL::TCL() - create mode\n");
if ( (m_basePage == nullptr) ||
@@ -501,10 +504,11 @@ bool TaskCenterLine::reject()
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
TaskDlgCenterLine::TaskDlgCenterLine(TechDraw::DrawViewPart* partFeat,
TechDraw::DrawPage* page,
- std::vector subNames)
+ std::vector subNames,
+ bool editMode)
: TaskDialog()
{
- widget = new TaskCenterLine(partFeat,page,subNames);
+ widget = new TaskCenterLine(partFeat,page,subNames, editMode);
taskbox = new Gui::TaskView::TaskBox(Gui::BitmapFactory().pixmap("actions/techdraw-facecenterline"),
widget->windowTitle(), true, 0);
taskbox->groupLayout()->addWidget(widget);
@@ -513,10 +517,11 @@ TaskDlgCenterLine::TaskDlgCenterLine(TechDraw::DrawViewPart* partFeat,
TaskDlgCenterLine::TaskDlgCenterLine(TechDraw::DrawViewPart* partFeat,
TechDraw::DrawPage* page,
- std::string edgeName)
+ std::string edgeName,
+ bool editMode)
: TaskDialog()
{
- widget = new TaskCenterLine(partFeat,page, edgeName);
+ widget = new TaskCenterLine(partFeat,page, edgeName, editMode);
taskbox = new Gui::TaskView::TaskBox(Gui::BitmapFactory().pixmap("actions/techdraw-facecenterline"),
widget->windowTitle(), true, 0);
taskbox->groupLayout()->addWidget(widget);
diff --git a/src/Mod/TechDraw/Gui/TaskCenterLine.h b/src/Mod/TechDraw/Gui/TaskCenterLine.h
index e63973e944..43f6fdb0dc 100644
--- a/src/Mod/TechDraw/Gui/TaskCenterLine.h
+++ b/src/Mod/TechDraw/Gui/TaskCenterLine.h
@@ -75,10 +75,12 @@ class TaskCenterLine : public QWidget
public:
TaskCenterLine(TechDraw::DrawViewPart* baseFeat,
TechDraw::DrawPage* page,
- std::vector subNames);
+ std::vector subNames,
+ bool editMode);
TaskCenterLine(TechDraw::DrawViewPart* baseFeat,
TechDraw::DrawPage* page,
- std::string edgeName);
+ std::string edgeName,
+ bool editMode);
~TaskCenterLine();
public Q_SLOTS:
@@ -145,6 +147,7 @@ private:
int m_clIdx;
int m_type;
int m_mode;
+ bool m_editMode;
};
class TaskDlgCenterLine : public Gui::TaskView::TaskDialog
@@ -154,10 +157,12 @@ class TaskDlgCenterLine : public Gui::TaskView::TaskDialog
public:
TaskDlgCenterLine(TechDraw::DrawViewPart* baseFeat,
TechDraw::DrawPage* page,
- std::vector subNames);
+ std::vector subNames,
+ bool editMode);
TaskDlgCenterLine(TechDraw::DrawViewPart* baseFeat,
TechDraw::DrawPage* page,
- std::string edgeName);
+ std::string edgeName,
+ bool editMode);
~TaskDlgCenterLine();
public: