diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt
index 47c529145a..9078159d2a 100644
--- a/src/Mod/Path/CMakeLists.txt
+++ b/src/Mod/Path/CMakeLists.txt
@@ -21,6 +21,7 @@ SET(PathScripts_SRCS
PathScripts/PathAreaOp.py
PathScripts/PathArray.py
PathScripts/PathCircularHoleBase.py
+ PathScripts/PathCircularHoleBaseGui.py
PathScripts/PathComment.py
PathScripts/PathCompoundExtended.py
PathScripts/PathCopy.py
@@ -42,6 +43,7 @@ SET(PathScripts_SRCS
PathScripts/PathFromShape.py
PathScripts/PathGeom.py
PathScripts/PathHelix.py
+ PathScripts/PathHelixGui.py
PathScripts/PathHop.py
PathScripts/PathInspect.py
PathScripts/PathJob.py
diff --git a/src/Mod/Path/Gui/Resources/Path.qrc b/src/Mod/Path/Gui/Resources/Path.qrc
index a0579984d0..4b0b526a27 100644
--- a/src/Mod/Path/Gui/Resources/Path.qrc
+++ b/src/Mod/Path/Gui/Resources/Path.qrc
@@ -66,6 +66,7 @@
panels/PageDepthsEdit.ui
panels/PageHeightsEdit.ui
panels/PageOpDrillingEdit.ui
+ panels/PageOpHelixEdit.ui
panels/PageOpPocketFullEdit.ui
panels/PageOpProfileFullEdit.ui
panels/PocketEdit.ui
diff --git a/src/Mod/Path/Gui/Resources/panels/PageOpHelixEdit.ui b/src/Mod/Path/Gui/Resources/panels/PageOpHelixEdit.ui
new file mode 100644
index 0000000000..a719b0f2a8
--- /dev/null
+++ b/src/Mod/Path/Gui/Resources/panels/PageOpHelixEdit.ui
@@ -0,0 +1,127 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 400
+ 229
+
+
+
+ Form
+
+
+ -
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+
-
+
+
+ Tool Controller
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
-
+
+
+ Start from
+
+
+
+ -
+
+
-
+
+ Inside
+
+
+ -
+
+ Outside
+
+
+
+
+ -
+
+
+ Direction
+
+
+
+ -
+
+
-
+
+ CW
+
+
+ -
+
+ CCW
+
+
+
+
+ -
+
+
+ Step over percent
+
+
+
+ -
+
+
+ 1
+
+
+ 100
+
+
+ 10
+
+
+ 100
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+
diff --git a/src/Mod/Path/InitGui.py b/src/Mod/Path/InitGui.py
index d817a6d8ab..0b10983ae6 100644
--- a/src/Mod/Path/InitGui.py
+++ b/src/Mod/Path/InitGui.py
@@ -58,7 +58,7 @@ class PathWorkbench (Workbench):
from PathScripts import PathFaceProfile
from PathScripts import PathFixture
from PathScripts import PathFromShape
- from PathScripts import PathHelix
+ from PathScripts import PathHelixGui
from PathScripts import PathHop
from PathScripts import PathInspect
from PathScripts import PathJob
diff --git a/src/Mod/Path/PathScripts/PathCircularHoleBase.py b/src/Mod/Path/PathScripts/PathCircularHoleBase.py
new file mode 100644
index 0000000000..5758a91e5a
--- /dev/null
+++ b/src/Mod/Path/PathScripts/PathCircularHoleBase.py
@@ -0,0 +1,224 @@
+# -*- coding: utf-8 -*-
+
+# ***************************************************************************
+# * *
+# * Copyright (c) 2017 sliptonic *
+# * *
+# * This program is free software; you can redistribute it and/or modify *
+# * it under the terms of the GNU Lesser General Public License (LGPL) *
+# * as published by the Free Software Foundation; either version 2 of *
+# * the License, or (at your option) any later version. *
+# * for detail see the LICENCE text file. *
+# * *
+# * This program is distributed in the hope that it will be useful, *
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+# * GNU Library General Public License for more details. *
+# * *
+# * You should have received a copy of the GNU Library General Public *
+# * License along with this program; if not, write to the Free Software *
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
+# * USA *
+# * *
+# ***************************************************************************
+
+import FreeCAD
+import DraftGeomUtils
+import PathScripts.PathLog as PathLog
+import PathScripts.PathOp as PathOp
+import PathScripts.PathUtils as PathUtils
+import string
+import sys
+
+from PySide import QtCore
+
+# Qt tanslation handling
+def translate(context, text, disambig=None):
+ return QtCore.QCoreApplication.translate(context, text, disambig)
+
+if True:
+ PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
+ PathLog.trackModule(PathLog.thisModule())
+
+class ObjectOp(PathOp.ObjectOp):
+
+ def opFeatures(self, obj):
+ return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureHeights | PathOp.FeatureBaseFaces | self.circularHoleFeatures(obj)
+
+ def initOperation(self, obj):
+ obj.addProperty("App::PropertyStringList", "Disabled", "Base", QtCore.QT_TRANSLATE_NOOP("Path", "List of disabled features"))
+ self.initCircularHoleOperation(obj)
+
+ def baseIsArchPanel(self, obj, base):
+ return hasattr(base, "Proxy") and isinstance(base.Proxy, ArchPanel.PanelSheet)
+
+ def getArchPanelEdge(self, obj, base, sub):
+ ids = string.split(sub, '.')
+ holeId = int(ids[0])
+ wireId = int(ids[1])
+ edgeId = int(ids[2])
+
+ for holeNr, hole in enumerate(base.Proxy.getHoles(base, transform=True)):
+ if holeNr == holeId:
+ for wireNr, wire in enumerate(hole.Wires):
+ if wireNr == wireId:
+ for edgeNr, edge in enumerate(wire.Edges):
+ if edgeNr == edgeId:
+ return edge
+
+ def holeDiameter(self, obj, base, sub):
+ if self.baseIsArchPanel(obj, base):
+ edge = self.getArchPanelEdge(obj, base, sub)
+ return edge.BoundBox.XLength
+
+ shape = base.Shape.getElement(sub)
+ if shape.ShapeType == 'Vertex':
+ return 0
+
+ # for all other shapes the diameter is just the dimension in X
+ return shape.BoundBox.XLength
+
+ def holePosition(self, obj, base, sub):
+ if self.baseIsArchPanel(obj, base):
+ edge = self.getArchPanelEdge(obj, base, sub)
+ center = edge.Curve.Center
+ return FreeCAD.Vector(center.x, center.y, 0)
+
+ shape = base.Shape.getElement(sub)
+ if shape.ShapeType == 'Vertex':
+ return FreeCAD.Vector(shape.X, shape.Y, 0)
+
+ if shape.ShapeType == 'Edge':
+ return FreeCAD.Vector(shape.Curve.Center.x, shape.Curve.Center.y, 0)
+
+ if shape.ShapeType == 'Face':
+ return FreeCAD.Vector(shape.Surface.Center.x, shape.Surface.Center.y, 0)
+
+ PathLog.error('This is bad')
+
+ def isHoleEnabled(self, obj, base, sub):
+ name = "%s.%s" % (base.Name, sub)
+ return not name in obj.Disabled
+
+ def opExecute(self, obj):
+ PathLog.track()
+
+ if len(obj.Base) == 0:
+ job = PathUtils.findParentJob(obj)
+ if not job or not job.Base:
+ return
+ baseobject = job.Base
+
+ # Arch PanelSheet
+ features = []
+ if self.baseIsArchPanel(obj, baseobject):
+ holeshapes = baseobject.Proxy.getHoles(baseobject, transform=True)
+ tooldiameter = obj.ToolController.Proxy.getTool(obj.ToolController).Diameter
+ for holeNr, hole in enumerate(holeshapes):
+ PathLog.debug('Entering new HoleShape')
+ for wireNr, wire in enumerate(hole.Wires):
+ PathLog.debug('Entering new Wire')
+ for edgeNr, edge in enumerate(wire.Edges):
+ if PathUtils.isDrillable(baseobject, edge, tooldiameter):
+ PathLog.debug('Found drillable hole edges: {}'.format(edge))
+ features.append((baseobject, "%d.%d.%d" % (holeNr, wireNr, edgeNr)))
+
+ self.setDepths(obj, None, None, baseobject.Shape.BoundBox)
+ else:
+ features = self.findHoles(obj, baseobject)
+ self.setupDepthsFrom(obj, features, baseobject)
+ obj.Base = features
+ obj.Disabled = []
+
+ holes = []
+
+ for base, subs in obj.Base:
+ for sub in subs:
+ if self.isHoleEnabled(obj, base, sub):
+ pos = self.holePosition(obj, base, sub)
+ holes.append({'x': pos.x, 'y': pos.y, 'r': self.holeDiameter(obj, base, sub)})
+
+ if len(holes) > 0:
+ self.circularHoleExecute(obj, holes)
+
+ def opOnChanged(self, obj, prop):
+ if 'Base' == prop and not 'Restore' in obj.State and obj.Base:
+ features = []
+ for base, subs in obj.Base:
+ for sub in subs:
+ features.append((base, sub))
+
+ job = PathUtils.findParentJob(obj)
+ if not job or not job.Base:
+ return
+
+ self.setupDepthsFrom(obj, features, job.Base)
+
+ def setupDepthsFrom(self, obj, features, baseobject):
+ zmax = None
+ zmin = None
+ for base,sub in features:
+ shape = base.Shape.getElement(sub)
+ bb = shape.BoundBox
+ # find the highes zmax and the highes zmin levels, those provide
+ # the safest values for StartDepth and FinalDepth
+ if zmax is None or zmax < bb.ZMax:
+ zmax = bb.ZMax
+ if zmin is None or zmin < bb.ZMin:
+ zmin = bb.ZMin
+ self.setDepths(obj, zmax, zmin, baseobject.Shape.BoundBox)
+
+ def setDepths(self, obj, zmax, zmin, bb):
+ PathLog.track(obj.Label, zmax, zmin, bb)
+ if zmax is None:
+ zmax = 5
+ if zmin is None:
+ zmin = 0
+
+ if zmin > zmax:
+ zmax = zmin
+
+ PathLog.debug("setDepths(%s): z=%.2f -> %.2f bb.z=%.2f -> %.2f" % (obj.Label, zmin, zmax, bb.ZMin, bb.ZMax))
+
+ obj.StartDepth = zmax
+ obj.ClearanceHeight = bb.ZMax + 5.0
+ obj.SafeHeight = bb.ZMax + 3.0
+ obj.FinalDepth = zmin
+
+ def findHoles(self, obj, baseobject):
+ shape = baseobject.Shape
+ PathLog.track('obj: {} shape: {}'.format(obj, shape))
+ holelist = []
+ features = []
+ # tooldiameter = obj.ToolController.Proxy.getTool(obj.ToolController).Diameter
+ tooldiameter = None
+ PathLog.debug('search for holes larger than tooldiameter: {}: '.format(tooldiameter))
+ if DraftGeomUtils.isPlanar(shape):
+ PathLog.debug("shape is planar")
+ for i in range(len(shape.Edges)):
+ candidateEdgeName = "Edge" + str(i + 1)
+ e = shape.getElement(candidateEdgeName)
+ if PathUtils.isDrillable(shape, e, tooldiameter):
+ PathLog.debug('edge candidate: {} (hash {})is drillable '.format(e, e.hashCode()))
+ x = e.Curve.Center.x
+ y = e.Curve.Center.y
+ diameter = e.BoundBox.XLength
+ holelist.append({'featureName': candidateEdgeName, 'feature': e, 'x': x, 'y': y, 'd': diameter, 'enabled': True})
+ features.append((baseobject, candidateEdgeName))
+ PathLog.debug("Found hole feature %s.%s" % (baseobject.Label, candidateEdgeName))
+ else:
+ PathLog.debug("shape is not planar")
+ for i in range(len(shape.Faces)):
+ candidateFaceName = "Face" + str(i + 1)
+ f = shape.getElement(candidateFaceName)
+ if PathUtils.isDrillable(shape, f, tooldiameter):
+ PathLog.debug('face candidate: {} is drillable '.format(f))
+ x = f.Surface.Center.x
+ y = f.Surface.Center.y
+ diameter = f.BoundBox.XLength
+ holelist.append({'featureName': candidateFaceName, 'feature': f, 'x': x, 'y': y, 'd': diameter, 'enabled': True})
+ features.append((baseobject, candidateFaceName))
+ PathLog.debug("Found hole feature %s.%s" % (baseobject.Label, candidateFaceName))
+
+ PathLog.debug("holes found: {}".format(holelist))
+ return features
diff --git a/src/Mod/Path/PathScripts/PathCircularHoleBaseGui.py b/src/Mod/Path/PathScripts/PathCircularHoleBaseGui.py
new file mode 100644
index 0000000000..f83e3e7298
--- /dev/null
+++ b/src/Mod/Path/PathScripts/PathCircularHoleBaseGui.py
@@ -0,0 +1,152 @@
+# -*- coding: utf-8 -*-
+
+# ***************************************************************************
+# * *
+# * Copyright (c) 2017 sliptonic *
+# * *
+# * This program is free software; you can redistribute it and/or modify *
+# * it under the terms of the GNU Lesser General Public License (LGPL) *
+# * as published by the Free Software Foundation; either version 2 of *
+# * the License, or (at your option) any later version. *
+# * for detail see the LICENCE text file. *
+# * *
+# * This program is distributed in the hope that it will be useful, *
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+# * GNU Library General Public License for more details. *
+# * *
+# * You should have received a copy of the GNU Library General Public *
+# * License along with this program; if not, write to the Free Software *
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
+# * USA *
+# * *
+# ***************************************************************************
+
+import FreeCAD
+import FreeCADGui
+import PathScripts.PathLog as PathLog
+import PathScripts.PathOpGui as PathOpGui
+
+from PySide import QtCore, QtGui
+
+if True:
+ PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
+ PathLog.trackModule(PathLog.thisModule())
+else:
+ PathLog.setLevel(PathLog.Level.NOTICE, PathLog.thisModule())
+
+class TaskPanelHoleGeometryPage(PathOpGui.TaskPanelBaseGeometryPage):
+ DataFeatureName = QtCore.Qt.ItemDataRole.UserRole
+ DataObject = QtCore.Qt.ItemDataRole.UserRole + 1
+ DataObjectSub = QtCore.Qt.ItemDataRole.UserRole + 2
+
+ def getForm(self):
+ return FreeCADGui.PySideUic.loadUi(":/panels/PageBaseHoleGeometryEdit.ui")
+
+ def setFields(self, obj):
+ PathLog.track()
+ self.form.baseList.blockSignals(True)
+ self.form.baseList.clearContents()
+ self.form.baseList.setRowCount(0)
+ for i, (base, subs) in enumerate(self.obj.Base):
+ for sub in subs:
+ self.form.baseList.insertRow(self.form.baseList.rowCount())
+
+ item = QtGui.QTableWidgetItem("%s.%s" % (base.Label, sub))
+ item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable)
+ if self.obj.Proxy.isHoleEnabled(self.obj, base, sub):
+ item.setCheckState(QtCore.Qt.Checked)
+ else:
+ item.setCheckState(QtCore.Qt.Unchecked)
+ name = "%s.%s" % (base.Name, sub)
+ item.setData(self.DataFeatureName, name)
+ item.setData(self.DataObject, base)
+ item.setData(self.DataObjectSub, sub)
+ self.form.baseList.setItem(self.form.baseList.rowCount()-1, 0, item)
+
+ item = QtGui.QTableWidgetItem("{:.3f}".format(self.obj.Proxy.holeDiameter(self.obj, base, sub)))
+ item.setData(self.DataFeatureName, name)
+ item.setData(self.DataObject, base)
+ item.setData(self.DataObjectSub, sub)
+ item.setTextAlignment(QtCore.Qt.AlignHCenter)
+ self.form.baseList.setItem(self.form.baseList.rowCount()-1, 1, item)
+
+ self.form.baseList.resizeColumnToContents(0)
+ self.form.baseList.blockSignals(False)
+
+ def itemActivated(self):
+ PathLog.track()
+ FreeCADGui.Selection.clearSelection()
+ activatedRows = []
+ for item in self.form.baseList.selectedItems():
+ row = item.row()
+ if not row in activatedRows:
+ activatedRows.append(row)
+ obj = item.data(self.DataObject)
+ sub = str(item.data(self.DataObjectSub))
+ PathLog.debug("itemActivated() -> %s.%s" % (obj.Label, sub))
+ if sub:
+ FreeCADGui.Selection.addSelection(obj, sub)
+ else:
+ FreeCADGui.Selection.addSelection(obj)
+ #FreeCADGui.updateGui()
+
+ def deleteBase(self):
+ PathLog.track()
+ deletedRows = []
+ selected = self.form.baseList.selectedItems()
+ for item in selected:
+ row = self.form.baseList.row(item)
+ if not row in deletedRows:
+ deletedRows.append(row)
+ self.form.baseList.removeRow(row)
+ self.updateBase()
+ #self.obj.Proxy.execute(self.obj)
+ FreeCAD.ActiveDocument.recompute()
+
+ def updateBase(self):
+ PathLog.track()
+ newlist = []
+ for i in range(self.form.baseList.rowCount()):
+ item = self.form.baseList.item(i, 0)
+ obj = item.data(self.DataObject)
+ sub = str(item.data(self.DataObjectSub))
+ base = (obj, sub)
+ PathLog.debug("keeping (%s.%s)" % (obj.Label, sub))
+ newlist.append(base)
+ PathLog.debug("obj.Base=%s newlist=%s" % (self.obj.Base, newlist))
+ self.obj.Base = newlist
+
+ def checkedChanged(self):
+ PathLog.track()
+ disabled = []
+ for i in xrange(0, self.form.baseList.rowCount()):
+ item = self.form.baseList.item(i, 0)
+ if item.checkState() != QtCore.Qt.Checked:
+ disabled.append(item.data(self.DataFeatureName))
+ self.obj.Disabled = disabled
+ FreeCAD.ActiveDocument.recompute()
+
+ def registerSignalHandlers(self, obj):
+ self.form.baseList.itemSelectionChanged.connect(self.itemActivated)
+ self.form.addBase.clicked.connect(self.addBase)
+ self.form.deleteBase.clicked.connect(self.deleteBase)
+ self.form.resetBase.clicked.connect(self.resetBase)
+ self.form.baseList.itemChanged.connect(self.checkedChanged)
+
+ def resetBase(self):
+ self.obj.Base = []
+ self.obj.Disabled = []
+
+ self.obj.Proxy.execute(self.obj)
+ FreeCAD.ActiveDocument.recompute()
+
+ def updateData(self, obj, prop):
+ if prop in ['Base', 'Disabled']:
+ self.setFields(self.obj)
+
+class TaskPanelOpPage(PathOpGui.TaskPanelPage):
+
+ def taskPanelBaseGeometryPage(self, obj, features):
+ return TaskPanelHoleGeometryPage(obj, features)
+
diff --git a/src/Mod/Path/PathScripts/PathDrilling.py b/src/Mod/Path/PathScripts/PathDrilling.py
index 4369648303..b7a57ed9ea 100644
--- a/src/Mod/Path/PathScripts/PathDrilling.py
+++ b/src/Mod/Path/PathScripts/PathDrilling.py
@@ -54,18 +54,17 @@ class ObjectDrilling(PathCircularHoleBase.ObjectOp):
# drilling works on anything
return PathOp.FeatureBaseGeometry
- def initOperation(self, obj):
+ def initCircularHoleOperation(self, obj):
- obj.addProperty("App::PropertyStringList", "Disabled", "Path", QtCore.QT_TRANSLATE_NOOP("App::Property", "List of disabled features"))
- obj.addProperty("App::PropertyLength", "PeckDepth", "Depth", QtCore.QT_TRANSLATE_NOOP("App::Property", "Incremental Drill depth before retracting to clear chips"))
- obj.addProperty("App::PropertyBool", "PeckEnabled", "Depth", QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable pecking"))
- obj.addProperty("App::PropertyFloat", "DwellTime", "Depth", QtCore.QT_TRANSLATE_NOOP("App::Property", "The time to dwell between peck cycles"))
- obj.addProperty("App::PropertyBool", "DwellEnabled", "Depth", QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable dwell"))
- obj.addProperty("App::PropertyBool", "AddTipLength", "Depth", QtCore.QT_TRANSLATE_NOOP("App::Property", "Calculate the tip length and subtract from final depth"))
- obj.addProperty("App::PropertyEnumeration", "ReturnLevel", "Depth", QtCore.QT_TRANSLATE_NOOP("App::Property", "Controls how tool retracts Default=G98"))
+ obj.addProperty("App::PropertyLength", "PeckDepth", "Drill", QtCore.QT_TRANSLATE_NOOP("App::Property", "Incremental Drill depth before retracting to clear chips"))
+ obj.addProperty("App::PropertyBool", "PeckEnabled", "Drill", QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable pecking"))
+ obj.addProperty("App::PropertyFloat", "DwellTime", "Drill", QtCore.QT_TRANSLATE_NOOP("App::Property", "The time to dwell between peck cycles"))
+ obj.addProperty("App::PropertyBool", "DwellEnabled", "Drill", QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable dwell"))
+ obj.addProperty("App::PropertyBool", "AddTipLength", "Drill", QtCore.QT_TRANSLATE_NOOP("App::Property", "Calculate the tip length and subtract from final depth"))
+ obj.addProperty("App::PropertyEnumeration", "ReturnLevel", "Drill", QtCore.QT_TRANSLATE_NOOP("App::Property", "Controls how tool retracts Default=G98"))
obj.ReturnLevel = ['G98', 'G99'] # this is the direction that the Contour runs
- obj.addProperty("App::PropertyDistance", "RetractHeight", "Depth", QtCore.QT_TRANSLATE_NOOP("App::Property", "The height where feed starts and height during retract tool when path is finished"))
+ obj.addProperty("App::PropertyDistance", "RetractHeight", "Drill", QtCore.QT_TRANSLATE_NOOP("App::Property", "The height where feed starts and height during retract tool when path is finished"))
def circularHoleExecute(self, obj, holes):
PathLog.track()
@@ -109,6 +108,13 @@ class ObjectDrilling(PathCircularHoleBase.ObjectOp):
self.commandlist.append(Path.Command('G80'))
+ def setDepths(self, obj, zmax, zmin, bb):
+ super(self.__class__, self).setDepths(obj, zmax, zmin, bb)
+ if zmax is not None:
+ obj.RetractHeight = zmax + 1.0
+ else:
+ obj.RetractHeight = 6.0
+
def opSetDefaultValues(self, obj):
obj.RetractHeight = 10
diff --git a/src/Mod/Path/PathScripts/PathDrillingGui.py b/src/Mod/Path/PathScripts/PathDrillingGui.py
index 7f3e485d9c..29c7e52523 100644
--- a/src/Mod/Path/PathScripts/PathDrillingGui.py
+++ b/src/Mod/Path/PathScripts/PathDrillingGui.py
@@ -24,10 +24,10 @@
import FreeCAD
import FreeCADGui
-import PathScripts.PathOpGui as PathOpGui
+import PathScripts.PathCircularHoleBaseGui as PathCircularHoleBaseGui
import PathScripts.PathDrilling as PathDrilling
import PathScripts.PathLog as PathLog
-import PathScripts.PathPocketBaseGui as PathPocketBaseGui
+import PathScripts.PathOpGui as PathOpGui
from PySide import QtCore, QtGui
@@ -37,117 +37,8 @@ if True:
else:
PathLog.setLevel(PathLog.Level.NOTICE, PathLog.thisModule())
-class TaskPanelHoleGeometryPage(PathOpGui.TaskPanelBaseGeometryPage):
- DataFeatureName = QtCore.Qt.ItemDataRole.UserRole
- DataObject = QtCore.Qt.ItemDataRole.UserRole + 1
- DataObjectSub = QtCore.Qt.ItemDataRole.UserRole + 2
- def getForm(self):
- return FreeCADGui.PySideUic.loadUi(":/panels/PageBaseHoleGeometryEdit.ui")
-
- def setFields(self, obj):
- PathLog.track()
- self.form.baseList.blockSignals(True)
- self.form.baseList.clearContents()
- self.form.baseList.setRowCount(0)
- for i, (base, subs) in enumerate(self.obj.Base):
- for sub in subs:
- self.form.baseList.insertRow(self.form.baseList.rowCount())
-
- item = QtGui.QTableWidgetItem("%s.%s" % (base.Label, sub))
- item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable)
- if self.obj.Proxy.isHoleEnabled(self.obj, base, sub):
- item.setCheckState(QtCore.Qt.Checked)
- else:
- item.setCheckState(QtCore.Qt.Unchecked)
- name = "%s.%s" % (base.Name, sub)
- item.setData(self.DataFeatureName, name)
- item.setData(self.DataObject, base)
- item.setData(self.DataObjectSub, sub)
- self.form.baseList.setItem(self.form.baseList.rowCount()-1, 0, item)
-
- item = QtGui.QTableWidgetItem("{:.3f}".format(self.obj.Proxy.holeDiameter(self.obj, base, sub)))
- item.setData(self.DataFeatureName, name)
- item.setData(self.DataObject, base)
- item.setData(self.DataObjectSub, sub)
- item.setTextAlignment(QtCore.Qt.AlignHCenter)
- self.form.baseList.setItem(self.form.baseList.rowCount()-1, 1, item)
-
- self.form.baseList.resizeColumnToContents(0)
- self.form.baseList.blockSignals(False)
-
- def itemActivated(self):
- PathLog.track()
- FreeCADGui.Selection.clearSelection()
- activatedRows = []
- for item in self.form.baseList.selectedItems():
- row = item.row()
- if not row in activatedRows:
- activatedRows.append(row)
- obj = item.data(self.DataObject)
- sub = str(item.data(self.DataObjectSub))
- PathLog.debug("itemActivated() -> %s.%s" % (obj.Label, sub))
- if sub:
- FreeCADGui.Selection.addSelection(obj, sub)
- else:
- FreeCADGui.Selection.addSelection(obj)
- #FreeCADGui.updateGui()
-
- def deleteBase(self):
- PathLog.track()
- deletedRows = []
- selected = self.form.baseList.selectedItems()
- for item in selected:
- row = self.form.baseList.row(item)
- if not row in deletedRows:
- deletedRows.append(row)
- self.form.baseList.removeRow(row)
- self.updateBase()
- #self.obj.Proxy.execute(self.obj)
- FreeCAD.ActiveDocument.recompute()
-
- def updateBase(self):
- PathLog.track()
- newlist = []
- for i in range(self.form.baseList.rowCount()):
- item = self.form.baseList.item(i, 0)
- obj = item.data(self.DataObject)
- sub = str(item.data(self.DataObjectSub))
- base = (obj, sub)
- PathLog.debug("keeping (%s.%s)" % (obj.Label, sub))
- newlist.append(base)
- PathLog.debug("obj.Base=%s newlist=%s" % (self.obj.Base, newlist))
- self.obj.Base = newlist
-
- def checkedChanged(self):
- PathLog.track()
- disabled = []
- for i in xrange(0, self.form.baseList.rowCount()):
- item = self.form.baseList.item(i, 0)
- if item.checkState() != QtCore.Qt.Checked:
- disabled.append(item.data(self.DataFeatureName))
- self.obj.Disabled = disabled
- FreeCAD.ActiveDocument.recompute()
-
- def registerSignalHandlers(self, obj):
- self.form.baseList.itemSelectionChanged.connect(self.itemActivated)
- self.form.addBase.clicked.connect(self.addBase)
- self.form.deleteBase.clicked.connect(self.deleteBase)
- self.form.resetBase.clicked.connect(self.resetBase)
- self.form.baseList.itemChanged.connect(self.checkedChanged)
-
- def resetBase(self):
- self.obj.Base = []
- self.obj.Disabled = []
-
- self.obj.Proxy.execute(self.obj)
- FreeCAD.ActiveDocument.recompute()
-
- def updateData(self, obj, prop):
- if prop in ['Base', 'Disabled']:
- self.setFields(self.obj)
-
-class TaskPanelOpPage(PathOpGui.TaskPanelPage):
+class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage):
def getForm(self):
return FreeCADGui.PySideUic.loadUi(":/panels/PageOpDrillingEdit.ui")
@@ -190,16 +81,16 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
def getSignalsForUpdate(self, obj):
signals = []
+
signals.append(self.form.retractHeight.editingFinished)
signals.append(self.form.peckDepth.editingFinished)
signals.append(self.form.dwellTime.editingFinished)
signals.append(self.form.dwellEnabled.stateChanged)
signals.append(self.form.peckEnabled.stateChanged)
signals.append(self.form.useTipLength.stateChanged)
- return signals
+ signals.append(self.form.toolController.currentIndexChanged)
- def taskPanelBaseGeometryPage(self, obj, features):
- return TaskPanelHoleGeometryPage(obj, features)
+ return signals
PathOpGui.SetupOperation('Drilling',
PathDrilling.Create,
diff --git a/src/Mod/Path/PathScripts/PathHelix.py b/src/Mod/Path/PathScripts/PathHelix.py
index 197b274387..9479c7df50 100644
--- a/src/Mod/Path/PathScripts/PathHelix.py
+++ b/src/Mod/Path/PathScripts/PathHelix.py
@@ -22,11 +22,17 @@
# * *
# ***************************************************************************
-from . import PathUtils
-from .PathUtils import fmt
-import Part
import FreeCAD
+import Part
import Path
+
+import PathScripts.PathCircularHoleBase as PathCircularHoleBase
+import PathScripts.PathLog as PathLog
+import PathScripts.PathOp as PathOp
+import PathScripts.PathUtils as PathUtils
+
+from PathUtils import fmt
+
if FreeCAD.GuiUp:
import FreeCADGui
from PySide import QtCore, QtGui
@@ -55,7 +61,6 @@ def connected(edge, face):
def cylinders_in_selection():
- from Part import Cylinder
selections = FreeCADGui.Selection.getSelectionEx()
cylinders = []
@@ -66,132 +71,13 @@ def cylinders_in_selection():
for feature in selection.SubElementNames:
subobj = getattr(base.Shape, feature)
if subobj.ShapeType == 'Face':
- if isinstance(subobj.Surface, Cylinder):
+ if isinstance(subobj.Surface, Part.Cylinder):
if z_cylinder(subobj):
cylinders[-1][1].append(feature)
return cylinders
-def helix_cut(center, r_out, r_in, dr, zmax, zmin, dz, safe_z, tool_diameter, vfeed, hfeed, direction, startside):
- """
- center: 2-tuple
- (x0, y0) coordinates of center
- r_out, r_in: floats
- radial range, cut from outer radius r_out in layers of dr to inner radius r_in
- zmax, zmin: floats
- z-range, cut from zmax in layers of dz down to zmin
- safe_z: float
- safety layer height
- tool_diameter: float
- Width of tool
- """
- from numpy import ceil, linspace
-
- if (zmax <= zmin):
- return
-
- out = "(helix_cut <{0}, {1}>, {2})".format(center[0], center[1],
- ", ".join(map(str, (r_out, r_in, dr, zmax, zmin, dz, safe_z,
- tool_diameter, vfeed, hfeed, direction, startside))))
-
- x0, y0 = center
- nz = max(int(ceil((zmax - zmin)/dz)), 2)
- zi = linspace(zmax, zmin, 2 * nz + 1)
-
- if dr > tool_diameter:
- FreeCAD.Console.PrintWarning("PathHelix: Warning, shortening dr to tool diameter!\n")
- dr = tool_diameter
-
- def xyz(x=None, y=None, z=None):
- out = ""
- if x is not None:
- out += " X" + fmt(x)
- if y is not None:
- out += " Y" + fmt(y)
- if z is not None:
- out += " Z" + fmt(z)
- return out
-
- def rapid(x=None, y=None, z=None):
- return "G0" + xyz(x, y, z) + "\n"
-
- def F(f=None):
- return (" F" + fmt(f) if f else "")
-
- def feed(x=None, y=None, z=None, f=None):
- return "G1" + xyz(x, y, z) + F(f) + "\n"
-
- def arc(x, y, i, j, z, f):
- if direction == "CW":
- code = "G2"
- elif direction == "CCW":
- code = "G3"
- return code + " I" + fmt(i) + " J" + fmt(j) + " X" + fmt(x) + " Y" + fmt(y) + " Z" + fmt(z) + F(f) + "\n"
-
- def helix_cut_r(r):
- out = ""
- out += rapid(x=x0+r, y=y0)
- out += rapid(z=zmax + tool_diameter)
- out += feed(z=zmax, f=vfeed)
- # z = zmin
- for i in range(1, nz+1):
- out += arc(x0-r, y0, i=-r, j=0.0, z=zi[2*i-1], f=hfeed)
- out += arc(x0+r, y0, i= r, j=0.0, z=zi[2*i], f=hfeed)
- out += arc(x0-r, y0, i=-r, j=0.0, z=zmin, f=hfeed)
- out += arc(x0+r, y0, i=r, j=0.0, z=zmin, f=hfeed)
- out += feed(z=zmax + tool_diameter, f=vfeed)
- out += rapid(z=safe_z)
- return out
-
- assert(r_out > 0.0)
- assert(r_in >= 0.0)
-
- msg = None
- if r_out < 0.0:
- msg = "r_out < 0"
- elif r_in > 0 and r_out - r_in < tool_diameter:
- msg = "r_out - r_in = {0} is < tool diameter of {1}".format(r_out - r_in, tool_diameter)
- elif r_in == 0.0 and not r_out > tool_diameter/2.:
- msg = "Cannot drill a hole of diameter {0} with a tool of diameter {1}".format(2 * r_out, tool_diameter)
- elif startside not in ["inside", "outside"]:
- msg = "Invalid value for parameter 'startside'"
-
- if msg:
- out += "(ERROR: Hole at {0}:".format((x0, y0, zmax)) + msg + ")\n"
- FreeCAD.Console.PrintError("PathHelix: Hole at {0}:".format((x0, y0, zmax)) + msg + "\n")
- return out
-
- if r_in > 0:
- out += "(annulus mode)\n"
- r_out = r_out - tool_diameter/2
- r_in = r_in + tool_diameter/2
- if abs((r_out - r_in) / dr) < 1e-5:
- radii = [(r_out + r_in)/2]
- else:
- nr = max(int(ceil((r_out - r_in)/dr)), 2)
- radii = linspace(r_out, r_in, nr)
- elif r_out <= 2 * dr:
- out += "(single helix mode)\n"
- radii = [r_out - tool_diameter/2]
- assert(radii[0] > 0)
- else:
- out += "(full hole mode)\n"
- r_out = r_out - tool_diameter/2
- r_in = dr/2
-
- nr = max(1 + int(ceil((r_out - r_in)/dr)), 2)
- radii = linspace(r_out, r_in, nr)
- assert(all(radii > 0))
-
- if startside == "inside":
- radii = radii[::-1]
-
- for r in radii:
- out += "(radius {0})\n".format(r)
- out += helix_cut_r(r)
-
- return out
def features_by_centers(base, features):
@@ -228,644 +114,165 @@ def features_by_centers(base, features):
return by_centers
-class ObjectPathHelix(object):
+class ObjectHelix(PathCircularHoleBase.ObjectOp):
- def __init__(self, obj):
+ def circularHoleFeatures(self, obj):
+ return PathOp.FeatureStepDown | PathOp.FeatureBaseEdges | PathOp.FeatureBaseFaces | PathOp.FeatureBasePanels
+
+ def initCircularHoleOperation(self, obj):
# Basic
- obj.addProperty("App::PropertyLink", "ToolController", "Path",
- translate("App::Property", "The tool controller that will be used to calculate the path"))
- obj.addProperty("App::PropertyLinkSubList", "Features", "Path",
- translate("Features", "Selected features for the drill operation"))
- obj.addProperty("App::PropertyBool", "Active", "Path",
- translate("Active", "Set to False to disable code generation"))
- obj.addProperty("App::PropertyString", "Comment", "Path",
- translate("Comment", "An optional comment for this profile, will appear in G-Code"))
-
- # Helix specific
- obj.addProperty("App::PropertyEnumeration", "Direction", "Helix Drill",
- translate("Direction", "The direction of the circular cuts, clockwise (CW), or counter clockwise (CCW)"))
+ obj.addProperty("App::PropertyEnumeration", "Direction", "Helix Drill", translate("PathHelix", "The direction of the circular cuts, clockwise (CW), or counter clockwise (CCW)"))
obj.Direction = ['CW', 'CCW']
- obj.addProperty("App::PropertyEnumeration", "StartSide", "Helix Drill",
- translate("Direction", "Start cutting from the inside or outside"))
- obj.StartSide = ['inside', 'outside']
+ obj.addProperty("App::PropertyEnumeration", "StartSide", "Helix Drill", translate("PathHelix", "Start cutting from the inside or outside"))
+ obj.StartSide = ['Inside', 'Outside']
- obj.addProperty("App::PropertyLength", "DeltaR", "Helix Drill",
- translate("DeltaR", "Radius increment (must be smaller than tool diameter)"))
+ obj.addProperty("App::PropertyLength", "StepOver", "Helix Drill", translate("PathHelix", "Radius increment (must be smaller than tool diameter)"))
- # Depth Properties
- obj.addProperty("App::PropertyDistance", "Clearance", "Depths",
- translate("Clearance", "Safe distance above the top of the hole to which to retract the tool"))
- obj.addProperty("App::PropertyLength", "StepDown", "Depths",
- translate("StepDown", "Incremental Step Down of Tool"))
- obj.addProperty("App::PropertyBool", "UseStartDepth", "Depths",
- translate("Use Start Depth", "Set to True to manually specify a start depth"))
- obj.addProperty("App::PropertyDistance", "StartDepth", "Depths",
- translate("Start Depth", "Starting Depth of Tool - first cut depth in Z"))
- obj.addProperty("App::PropertyBool", "UseFinalDepth", "Depths",
- translate("Use Final Depth", "Set to True to manually specify a final depth"))
- obj.addProperty("App::PropertyDistance", "FinalDepth", "Depths",
- translate("Final Depth", "Final Depth of Tool - lowest value in Z"))
- obj.addProperty("App::PropertyDistance", "ThroughDepth", "Depths",
- translate("Through Depth", "Add this amount of additional cutting depth "
- "to open-ended holes. Only used if UseFinalDepth is False"))
+ def circularHoleExecute(self, obj, holes):
+ PathLog.track()
+ self.commandlist.append(Path.Command('(helix cut operation)'))
- # The current tool number, read-only
- # this is apparently used internally, to keep track of tool chagnes
- obj.addProperty("App::PropertyIntegerConstraint", "ToolNumber", "Tool",
- translate("PathProfile", "The current tool in use"))
- obj.ToolNumber = (0, 0, 1000, 1)
- obj.setEditorMode('ToolNumber', 1) # make this read only
+ self.commandlist.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid}))
- obj.Proxy = self
+ zsafe = max(baseobj.Shape.BoundBox.ZMax for baseobj, features in obj.Base) + obj.ClearanceHeight.Value
+ output = ''
+ output += "G0 Z" + fmt(zsafe)
- def __getstate__(self):
- return None
+ for hole in holes:
+ output += self.helix_cut(obj, hole['x'], hole['y'], hole['r'] / 2, 0.0, (float(obj.StepOver.Value)/50.0) * self.radius)
+ PathLog.debug(output)
- def __setstate__(self, state):
- return None
+ def helix_cut(self, obj, x0, y0, r_out, r_in, dr):
+ """
+ x0, y0:
+ coordinates of center
+ r_out, r_in: floats
+ radial range, cut from outer radius r_out in layers of dr to inner radius r_in
+ zmax, zmin: floats
+ z-range, cut from zmax in layers of dz down to zmin
+ safe_z: float
+ safety layer height
+ tool_diameter: float
+ Width of tool
+ """
+ from numpy import ceil, linspace
- def execute(self, obj):
- # from Part import Circle, Cylinder, Plane
- # from PathScripts import PathUtils
- # from math import sqrt
+ if (obj.StartDepth.Value <= obj.FinalDepth.Value):
+ return
- output = '(helix cut operation'
- if obj.Comment:
- output += ', ' + str(obj.Comment) + ')\n'
+ out = "(helix_cut <{0}, {1}>, {2})".format(x0, y0,
+ ", ".join(map(str, (r_out, r_in, dr, obj.StartDepth.Value, obj.FinalDepth.Value, obj.StepDown.Value, obj.SafeHeight.Value,
+ self.radius, self.vertFeed, self.horizFeed, obj.Direction, obj.StartSide))))
+
+ nz = max(int(ceil((obj.StartDepth.Value - obj.FinalDepth.Value)/obj.StepDown.Value)), 2)
+ zi = linspace(obj.StartDepth.Value, obj.FinalDepth.Value, 2 * nz + 1)
+
+ def xyz(x=None, y=None, z=None):
+ out = ""
+ if x is not None:
+ out += " X" + fmt(x)
+ if y is not None:
+ out += " Y" + fmt(y)
+ if z is not None:
+ out += " Z" + fmt(z)
+ return out
+
+ def rapid(x=None, y=None, z=None):
+ return "G0" + xyz(x, y, z) + "\n"
+
+ def F(f=None):
+ return (" F" + fmt(f) if f else "")
+
+ def feed(x=None, y=None, z=None, f=None):
+ return "G1" + xyz(x, y, z) + F(f) + "\n"
+
+ def arc(x, y, i, j, z, f):
+ if obj.Direction == "CW":
+ code = "G2"
+ elif obj.Direction == "CCW":
+ code = "G3"
+ return code + " I" + fmt(i) + " J" + fmt(j) + " X" + fmt(x) + " Y" + fmt(y) + " Z" + fmt(z) + F(f) + "\n"
+
+ def helix_cut_r(r):
+ arc_cmd = 'G2' if obj.Direction == 'CW' else 'G3'
+ out = ""
+ out += rapid(x=x0+r, y=y0)
+ self.commandlist.append(Path.Command('G0', {'X': x0 + r, 'Y': y0, 'F': self.horizRapid}))
+ out += rapid(z=obj.StartDepth.Value + 2*self.radius)
+ self.commandlist.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
+ out += feed(z=obj.StartDepth.Value, f=self.vertFeed)
+ self.commandlist.append(Path.Command('G1', {'Z': obj.StartDepth.Value, 'F': self.vertFeed}))
+ # z = obj.FinalDepth.Value
+ for i in range(1, nz+1):
+ out += arc(x0-r, y0, i=-r, j=0.0, z=zi[2*i-1], f=self.horizFeed)
+ self.commandlist.append(Path.Command(arc_cmd, {'X': x0-r, 'Y': y0, 'Z': zi[2*i-1], 'I': -r, 'J': 0.0, 'F': self.horizFeed}))
+ out += arc(x0+r, y0, i= r, j=0.0, z=zi[2*i], f=self.horizFeed)
+ self.commandlist.append(Path.Command(arc_cmd, {'X': x0+r, 'Y': y0, 'Z': zi[2*i], 'I': r, 'J': 0.0, 'F': self.horizFeed}))
+ out += arc(x0-r, y0, i=-r, j=0.0, z=obj.FinalDepth.Value, f=self.horizFeed)
+ self.commandlist.append(Path.Command(arc_cmd, {'X': x0-r, 'Y': y0, 'Z': obj.FinalDepth.Value, 'I': -r, 'J': 0.0, 'F': self.horizFeed}))
+ out += arc(x0+r, y0, i=r, j=0.0, z=obj.FinalDepth.Value, f=self.horizFeed)
+ self.commandlist.append(Path.Command(arc_cmd, {'X': x0+r, 'Y': y0, 'Z': obj.FinalDepth.Value, 'I': r, 'J': 0.0, 'F': self.horizFeed}))
+ out += feed(z=obj.StartDepth.Value + 2*self.radius, f=self.vertFeed)
+ out += rapid(z=obj.SafeHeight.Value)
+ self.commandlist.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
+ return out
+
+ assert(r_out > 0.0)
+ assert(r_in >= 0.0)
+
+ msg = None
+ if r_out < 0.0:
+ msg = "r_out < 0"
+ elif r_in > 0 and r_out - r_in < 2*self.radius:
+ msg = "r_out - r_in = {0} is < tool diameter of {1}".format(r_out - r_in, 2*self.radius)
+ elif r_in == 0.0 and not r_out > self.radius/2.:
+ msg = "Cannot drill a hole of diameter {0} with a tool of diameter {1}".format(2 * r_out, 2*self.radius)
+ elif obj.StartSide not in ["Inside", "Outside"]:
+ msg = "Invalid value for parameter 'obj.StartSide'"
+
+ if msg:
+ out += "(ERROR: Hole at {0}:".format((x0, y0, obj.StartDepth.Value)) + msg + ")\n"
+ PathLog.error("PathHelix: Hole at {0}:".format((x0, y0, obj.StartDepth.Value)) + msg + "\n")
+ return out
+
+ if r_in > 0:
+ out += "(annulus mode)\n"
+ r_out = r_out - self.radius
+ r_in = r_in + self.radius
+ if abs((r_out - r_in) / dr) < 1e-5:
+ radii = [(r_out + r_in)/2]
+ else:
+ nr = max(int(ceil((r_out - r_in)/dr)), 2)
+ radii = linspace(r_out, r_in, nr)
+ elif r_out <= 2 * dr:
+ out += "(single helix mode)\n"
+ radii = [r_out - self.radius]
+ assert(radii[0] > 0)
else:
- output += ')\n'
+ out += "(full hole mode)\n"
+ r_out = r_out - self.radius
+ r_in = dr/2
- if obj.Features:
- if not obj.Active:
- obj.Path = Path.Path("(helix cut operation inactive)")
- if obj.ViewObject:
- obj.ViewObject.Visibility = False
- return
+ nr = max(1 + int(ceil((r_out - r_in)/dr)), 2)
+ radii = linspace(r_out, r_in, nr)
+ assert(all(radii > 0))
- if not obj.ToolController:
- obj.ToolController = PathUtils.findToolController(obj)
+ if obj.StartSide == "Inside":
+ radii = radii[::-1]
- toolLoad = obj.ToolController
+ for r in radii:
+ out += "(radius {0})\n".format(r)
+ out += helix_cut_r(r)
- if toolLoad is None or toolLoad.ToolNumber == 0:
- FreeCAD.Console.PrintError("PathHelix: No tool selected for helix cut operation, insert a tool change operation first\n")
- obj.Path = Path.Path("(ERROR: no tool selected for helix cut operation)")
- return
-
- tool = toolLoad.Proxy.getTool(toolLoad)
-
- zsafe = max(baseobj.Shape.BoundBox.ZMax for baseobj, features in obj.Features) + obj.Clearance.Value
- output += "G0 Z" + fmt(zsafe)
-
- drill_jobs = []
-
- for base, features in obj.Features:
- for center, by_radius in features_by_centers(base, features).items():
- radii = sorted(by_radius.keys(), reverse=True)
- cylinders = map(lambda radius: getattr(base.Shape, by_radius[radius]), radii)
- zsafe = max(cyl.BoundBox.ZMax for cyl in cylinders) + obj.Clearance.Value
- cur_z = cylinders[0].BoundBox.ZMax
- jobs = []
-
- for cylinder in cylinders:
- # Find other edge of current cylinder
- other_edge = None
- for edge in cylinder.Edges:
- if isinstance(edge.Curve, Part.Circle) and edge.Curve.Center.z != cur_z:
- other_edge = edge
- break
-
- next_z = other_edge.Curve.Center.z
- dz = next_z - cur_z
- r = cylinder.Surface.Radius
-
- if dz < 0:
- # This is a closed hole if the face connected to
- # the current cylinder at next_z has the cylinder's
- # edge as its OuterWire
- closed = None
- for face in base.Shape.Faces:
- if connected(other_edge, face) and not face.isSame(cylinder.Faces[0]):
- wire = face.OuterWire
- if len(wire.Edges) == 1 and wire.Edges[0].isSame(other_edge):
- closed = True
- else:
- closed = False
-
- if closed is None:
- raise Exception("Cannot determine if this cylinder is closed on the z = {0} side".format(next_z))
-
- xc, yc, _ = cylinder.Surface.Center
- jobs.append(dict(xc=xc, yc=yc,
- zmin=next_z, zmax=cur_z, zsafe=zsafe,
- r_out=r, r_in=0.0, closed=closed))
-
- elif dz > 0:
- new_jobs = []
- for job in jobs:
- if job["zmin"] < next_z < job["zmax"]:
- # split this job
- job1 = dict(job)
- job2 = dict(job)
- job1["zmin"] = next_z
- job2["zmax"] = next_z
- job2["r_in"] = r
- new_jobs.append(job1)
- new_jobs.append(job2)
- else:
- new_jobs.append(job)
- jobs = new_jobs
- else:
- FreeCAD.Console.PrintError("PathHelix: Encountered cylinder with zero height\n")
- break
-
- cur_z = next_z
-
- if obj.UseStartDepth:
- jobs = [job for job in jobs if job["zmin"] < obj.StartDepth.Value]
- if jobs:
- jobs[0]["zmax"] = obj.StartDepth.Value
- if obj.UseFinalDepth:
- jobs = [job for job in jobs if job["zmax"] > obj.FinalDepth.Value]
- if jobs:
- jobs[-1]["zmin"] = obj.FinalDepth.Value
- else:
- if not jobs[-1]["closed"]:
- jobs[-1]["zmin"] -= obj.ThroughDepth.Value
-
- drill_jobs.extend(jobs)
-
- if len(drill_jobs) > 0:
- drill_jobs = PathUtils.sort_jobs(drill_jobs, ['xc', 'yc'], ['xc', 'zmax'])
-
- for job in drill_jobs:
- output += helix_cut((job["xc"], job["yc"]), job["r_out"], job["r_in"], obj.DeltaR.Value,
- job["zmax"], job["zmin"], obj.StepDown.Value,
- job["zsafe"], tool.Diameter,
- toolLoad.VertFeed.Value, toolLoad.HorizFeed.Value,
- obj.Direction, obj.StartSide)
- output += '\n'
-
- obj.Path = Path.Path(output)
- # if obj.ViewObject:
- # obj.ViewObject.Visibility = True
-
-
-class ViewProviderPathHelix(object):
- def __init__(self, vobj):
- vobj.Proxy = self
-
- def attach(self, vobj):
- self.Object = vobj.Object
- return
-
- def getIcon(self):
- return ":/icons/Path-Helix.svg"
-
- def setEdit(self, vobj, mode=0):
- FreeCADGui.Control.closeDialog()
- taskpanel = TaskPanel(vobj.Object)
- FreeCADGui.Control.showDialog(taskpanel)
- return True
-
- def __getstate__(self):
- return None
-
- def __setstate__(self, state):
- return None
-
-
-class CommandPathHelix(object):
- def GetResources(self):
- return {'Pixmap': 'Path-Helix',
- 'MenuText': QtCore.QT_TRANSLATE_NOOP("PathHelix", "PathHelix"),
- 'ToolTip': QtCore.QT_TRANSLATE_NOOP("PathHelix", "Creates a helix cut from selected circles")}
-
- def IsActive(self):
- if FreeCAD.ActiveDocument is not None:
- for o in FreeCAD.ActiveDocument.Objects:
- if o.Name[:3] == "Job":
- return True
- return False
-
- def Activated(self):
- import FreeCADGui
- # import Path
- from PathScripts import PathUtils
-
- FreeCAD.ActiveDocument.openTransaction(translate("PathHelix", "Create a helix cut"))
- FreeCADGui.addModule("PathScripts.PathHelix")
-
- obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", "PathHelix")
- ObjectPathHelix(obj)
- ViewProviderPathHelix(obj.ViewObject)
-
- obj.Features = cylinders_in_selection()
- obj.DeltaR = 1.0
-
- if not obj.ToolController:
- obj.ToolController = PathUtils.findToolController(obj)
-
- toolLoad = obj.ToolController
-
- if toolLoad is not None:
- obj.ToolNumber = toolLoad.ToolNumber
- tool = toolLoad.Proxy.getTool(toolLoad)
- if tool:
- # start with 25% overlap
- obj.DeltaR = tool.Diameter * 0.75
-
- obj.Active = True
- obj.Comment = ""
+ return out
+ def opSetDefaultValues(self, obj):
obj.Direction = "CW"
- obj.StartSide = "inside"
+ obj.StartSide = "Inside"
+ obj.StepOver = 100
- obj.Clearance = 10.0
- obj.StepDown = 1.0
- obj.UseStartDepth = False
- obj.StartDepth = 1.0
- obj.UseFinalDepth = False
- obj.FinalDepth = 0.0
- obj.ThroughDepth = 0.0
-
- PathUtils.addToJob(obj)
-
- obj.ViewObject.startEditing()
-
- FreeCAD.ActiveDocument.recompute()
-
-
-def print_exceptions(func):
- from functools import wraps
- import traceback
- import sys
-
- @wraps(func)
- def wrapper(*args, **kwargs):
- try:
- return func(*args, **kwargs)
- except:
- ex_type, ex, tb = sys.exc_info()
- FreeCAD.Console.PrintError("".join(traceback.format_exception(ex_type, ex, tb)) + "\n")
- raise
-
- return wrapper
-
-
-def print_all_exceptions(cls):
- for entry in dir(cls):
- obj = getattr(cls, entry)
- if not entry.startswith("__") and hasattr(obj, "__call__"):
- setattr(cls, entry, print_exceptions(obj))
- return cls
-
-
-@print_all_exceptions
-class TaskPanel(object):
-
- def __init__(self, obj):
- #from Units import Quantity
- from PathScripts import PathUtils
-
- self.obj = obj
- self.previous_value = {}
- self.form = QtGui.QToolBox()
-
- ui = FreeCADGui.UiLoader()
-
- grayed_out = "background-color: #d0d0d0;"
-
- def nextToolBoxItem(label, iconFile):
- widget = QtGui.QWidget()
- layout = QtGui.QGridLayout()
- widget.setLayout(layout)
- icon = QtGui.QIcon(iconFile)
- self.form.addItem(widget, icon, label)
- return layout
-
- def addFiller():
- row = layout.rowCount()
- widget = QtGui.QWidget()
- layout.addWidget(widget, row, 0, 1, 2)
- layout.setRowStretch(row, 1)
-
- layout = nextToolBoxItem("Geometry", ":/icons/PartDesign_InternalExternalGear.svg")
-
- def addWidget(widget):
- row = layout.rowCount()
- layout.addWidget(widget, row, 0, 1, 2)
-
- def addWidgets(widget1, widget2):
- row = layout.rowCount()
- layout.addWidget(widget1, row, 0)
- layout.addWidget(widget2, row, 1)
-
- def addQuantity(property, labelstring, activator=None, max=None):
- self.previous_value[property] = getattr(self.obj, property)
- widget = ui.createWidget("Gui::InputField")
-
- if activator:
- self.previous_value[activator] = getattr(self.obj, activator)
- currently_active = getattr(self.obj, activator)
- label = QtGui.QCheckBox(labelstring)
-
- def change(state):
- setattr(self.obj, activator, label.isChecked())
- if label.isChecked():
- widget.setStyleSheet("")
- else:
- widget.setStyleSheet(grayed_out)
- self.obj.Proxy.execute(self.obj)
- FreeCAD.ActiveDocument.recompute()
-
- label.stateChanged.connect(change)
- label.setChecked(currently_active)
- if not currently_active:
- widget.setStyleSheet(grayed_out)
- label.setToolTip(self.obj.getDocumentationOfProperty(activator))
- else:
- label = QtGui.QLabel(labelstring)
- label.setToolTip(self.obj.getDocumentationOfProperty(property))
-
- quantity = getattr(self.obj, property)
- widget.setText(quantity.UserString)
- widget.setToolTip(self.obj.getDocumentationOfProperty(property))
-
- if max:
- # cannot use widget.setMaximum() as apparently ui.createWidget()
- # returns the object up-casted to QWidget.
- widget.setProperty("maximum", max)
-
- def change(quantity):
- setattr(self.obj, property, quantity)
- self.obj.Proxy.execute(self.obj)
- FreeCAD.ActiveDocument.recompute()
-
- QtCore.QObject.connect(widget, QtCore.SIGNAL("valueChanged(const Base::Quantity &)"), change)
-
- addWidgets(label, widget)
- return label, widget
-
- def addCheckBox(property, label):
- self.previous_value[property] = getattr(self.obj, property)
- widget = QtGui.QCheckBox(label)
- widget.setToolTip(self.obj.getDocumentationOfProperty(property))
-
- def change(state):
- setattr(self.obj, property, widget.isChecked())
- self.obj.Proxy.execute(self.obj)
- FreeCAD.ActiveDocument.recompute()
- widget.stateChanged.connect(change)
-
- widget.setChecked(getattr(self.obj, property))
- addWidget(widget)
-
- def addEnumeration(property, label, options):
- self.previous_value[property] = getattr(self.obj, property)
- label = QtGui.QLabel(label)
- label.setToolTip(self.obj.getDocumentationOfProperty(property))
- widget = QtGui.QComboBox()
- widget.setToolTip(self.obj.getDocumentationOfProperty(property))
- for option_label, option_value in options:
- widget.addItem(option_label)
-
- def change(index):
- setattr(self.obj, property, options[index][1])
- self.obj.Proxy.execute(self.obj)
- FreeCAD.ActiveDocument.recompute()
-
- widget.currentIndexChanged.connect(change)
- addWidgets(label, widget)
-
- self.featureTree = QtGui.QTreeWidget()
- self.featureTree.setMinimumHeight(200)
- self.featureTree.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)
- # self.featureTree.setDragDropMode(QtGui.QAbstractItemView.DragDrop)
- # self.featureTree.setDefaultDropAction(QtCore.Qt.MoveAction)
- self.fillFeatureTree()
- sm = self.featureTree.selectionModel()
- sm.selectionChanged.connect(self.selectFeatures)
- addWidget(self.featureTree)
- self.featureTree.expandAll()
-
- self.addButton = QtGui.QPushButton("Add holes")
- self.addButton.clicked.connect(self.addCylinders)
-
- self.delButton = QtGui.QPushButton("Delete")
- self.delButton.clicked.connect(self.delCylinders)
-
- addWidgets(self.addButton, self.delButton)
-
- # End of "Features" section
-
- layout = nextToolBoxItem("Drill parameters", ":/icons/Path-OperationB.svg")
- addCheckBox("Active", "Operation is active")
-
- toolLoad = PathUtils.findToolController(obj)
- tool = toolLoad and toolLoad.Proxy.getTool(toolLoad)
-
- if not tool:
- drmax = None
- else:
- drmax = tool.Diameter
-
- addQuantity("DeltaR", "Step in Radius", max=drmax)
- addQuantity("StepDown", "Step in Z")
- addEnumeration("Direction", "Cut direction",
- [("Clockwise", "CW"), ("Counter-Clockwise", "CCW")])
- addEnumeration("StartSide", "Start Side",
- [("Start from inside", "inside"), ("Start from outside", "outside")])
-
- # End of "Drill parameters" section
- addFiller()
-
- layout = nextToolBoxItem("Cutting Depths", ":/icons/Path-Depths.svg")
- addQuantity("Clearance", "Clearance Distance")
- addQuantity("StartDepth", "Absolute start height", "UseStartDepth")
-
- fdcheckbox, fdinput = addQuantity("FinalDepth", "Absolute final height", "UseFinalDepth")
- tdlabel, tdinput = addQuantity("ThroughDepth", "Extra drill depth\nfor open holes")
-
- # End of "Cutting Depths" section
- addFiller()
-
- # make ThroughDepth and FinalDepth mutually exclusive
- def fd_change(state):
- if fdcheckbox.isChecked():
- tdinput.setStyleSheet(grayed_out)
- else:
- tdinput.setStyleSheet("")
- fdcheckbox.stateChanged.connect(fd_change)
-
- def td_change(quantity):
- fdcheckbox.setChecked(False)
- QtCore.QObject.connect(tdinput, QtCore.SIGNAL("valueChanged(const Base::Quantity &)"), td_change)
-
- if obj.UseFinalDepth:
- tdinput.setStyleSheet(grayed_out)
-
- def addCylinders(self):
- features_per_base = {}
- for base, features in self.obj.Features:
- features_per_base[base] = list(set(features))
-
- for base, features in cylinders_in_selection():
- for feature in features:
- if base in features_per_base:
- if feature not in features_per_base[base]:
- features_per_base[base].append(feature)
- else:
- features_per_base[base] = [feature]
-
- self.obj.Features = list(features_per_base.items())
- self.featureTree.clear()
- self.fillFeatureTree()
- self.featureTree.expandAll()
- self.obj.Proxy.execute(self.obj)
- FreeCAD.ActiveDocument.recompute()
-
- def delCylinders(self):
- del_features = []
-
- def delete_feature(item, base=None):
- kind, feature = item.data(0, QtCore.Qt.UserRole)
- assert(kind == "feature")
-
- if base is None:
- base_item = item.parent().parent()
- _, base = base_item.data(0, QtCore.Qt.UserRole)
-
- del_features.append((base, feature))
- item.parent().takeChild(item.parent().indexOfChild(item))
-
- def delete_hole(item, base=None):
- kind, center = item.data(0, QtCore.Qt.UserRole)
- assert(kind == "hole")
-
- if base is None:
- base_item = item.parent()
- _, base = base_item.data(0, QtCore.Qt.UserRole)
-
- for i in reversed(range(item.childCount())):
- delete_feature(item.child(i), base=base)
- item.parent().takeChild(item.parent().indexOfChild(item))
-
- def delete_base(item):
- kind, base = item.data(0, QtCore.Qt.UserRole)
- assert(kind == "base")
- for i in reversed(range(item.childCount())):
- delete_hole(item.child(i), base=base)
- self.featureTree.takeTopLevelItem(self.featureTree.indexOfTopLevelItem(item))
-
- for item in self.featureTree.selectedItems():
- kind, info = item.data(0, QtCore.Qt.UserRole)
- if kind == "base":
- delete_base(item)
- elif kind == "hole":
- parent = item.parent()
- delete_hole(item)
- if parent.childCount() == 0:
- self.featureTree.takeTopLevelItem(self.featureTree.indexOfTopLevelItem(parent))
- elif kind == "feature":
- parent = item.parent()
- delete_feature(item)
- if parent.childCount() == 0:
- parent.parent().takeChild(parent.parent().indexOfChild(parent))
- else:
- raise Exception("No such item kind: {0}".format(kind))
-
- for base, features in cylinders_in_selection():
- for feature in features:
- del_features.append((base, feature))
-
- new_features = []
- for obj, features in self.obj.Features:
- for feature in features:
- if (obj, feature) not in del_features:
- new_features.append((obj, feature))
-
- self.obj.Features = new_features
- self.obj.Proxy.execute(self.obj)
- FreeCAD.ActiveDocument.recompute()
-
- def fillFeatureTree(self):
- for base, features in self.obj.Features:
- base_item = QtGui.QTreeWidgetItem()
- base_item.setText(0, base.Name)
- base_item.setData(0, QtCore.Qt.UserRole, ("base", base))
- self.featureTree.addTopLevelItem(base_item)
- for center, by_radius in features_by_centers(base, features).items():
- hole_item = QtGui.QTreeWidgetItem()
- hole_item.setText(0, "Hole at ({0[0]:.2f}, {0[1]:.2f})".format(center))
- hole_item.setData(0, QtCore.Qt.UserRole, ("hole", center))
- base_item.addChild(hole_item)
- for radius in sorted(by_radius.keys(), reverse=True):
- feature = by_radius[radius]
- cylinder = getattr(base.Shape, feature)
- cyl_item = QtGui.QTreeWidgetItem()
- cyl_item.setText(0, "Diameter {0:.2f}, {1}".format(
- 2 * cylinder.Surface.Radius, feature))
- cyl_item.setData(0, QtCore.Qt.UserRole, ("feature", feature))
- hole_item.addChild(cyl_item)
-
- def selectFeatures(self, selected, deselected):
- FreeCADGui.Selection.clearSelection()
-
- def select_feature(item, base=None):
- kind, feature = item.data(0, QtCore.Qt.UserRole)
- assert(kind == "feature")
-
- if base is None:
- base_item = item.parent().parent()
- _, base = base_item.data(0, QtCore.Qt.UserRole)
-
- FreeCADGui.Selection.addSelection(base, feature)
-
- def select_hole(item, base=None):
- kind, center = item.data(0, QtCore.Qt.UserRole)
- assert(kind == "hole")
-
- if base is None:
- base_item = item.parent()
- _, base = base_item.data(0, QtCore.Qt.UserRole)
-
- for i in range(item.childCount()):
- select_feature(item.child(i), base=base)
-
- def select_base(item):
- kind, base = item.data(0, QtCore.Qt.UserRole)
- assert(kind == "base")
-
- for i in range(item.childCount()):
- select_hole(item.child(i), base=base)
-
- for item in self.featureTree.selectedItems():
- kind, info = item.data(0, QtCore.Qt.UserRole)
-
- if kind == "base":
- select_base(item)
- elif kind == "hole":
- select_hole(item)
- elif kind == "feature":
- select_feature(item)
-
- def needsFullSpace(self):
- return True
-
- def accept(self):
- FreeCADGui.ActiveDocument.resetEdit()
- FreeCADGui.Control.closeDialog()
-
- def reject(self):
- for property in self.previous_value:
- setattr(self.obj, property, self.previous_value[property])
- self.obj.Proxy.execute(self.obj)
- FreeCAD.ActiveDocument.recompute()
- FreeCADGui.ActiveDocument.resetEdit()
- FreeCADGui.Control.closeDialog()
-
-if FreeCAD.GuiUp:
- # import FreeCADGui
- FreeCADGui.addCommand('Path_Helix', CommandPathHelix())
+def Create(name):
+ obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
+ proxy = ObjectHelix(obj)
+ return obj
diff --git a/src/Mod/Path/PathScripts/PathHelixGui.py b/src/Mod/Path/PathScripts/PathHelixGui.py
new file mode 100644
index 0000000000..78f88b2942
--- /dev/null
+++ b/src/Mod/Path/PathScripts/PathHelixGui.py
@@ -0,0 +1,81 @@
+# -*- coding: utf-8 -*-
+
+# ***************************************************************************
+# * *
+# * Copyright (c) 2017 sliptonic *
+# * *
+# * This program is free software; you can redistribute it and/or modify *
+# * it under the terms of the GNU Lesser General Public License (LGPL) *
+# * as published by the Free Software Foundation; either version 2 of *
+# * the License, or (at your option) any later version. *
+# * for detail see the LICENCE text file. *
+# * *
+# * This program is distributed in the hope that it will be useful, *
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+# * GNU Library General Public License for more details. *
+# * *
+# * You should have received a copy of the GNU Library General Public *
+# * License along with this program; if not, write to the Free Software *
+# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
+# * USA *
+# * *
+# ***************************************************************************
+
+import FreeCAD
+import FreeCADGui
+import PathScripts.PathCircularHoleBaseGui as PathCircularHoleBaseGui
+import PathScripts.PathHelix as PathHelix
+import PathScripts.PathLog as PathLog
+import PathScripts.PathOpGui as PathOpGui
+
+from PySide import QtCore, QtGui
+
+if True:
+ PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
+ PathLog.trackModule(PathLog.thisModule())
+else:
+ PathLog.setLevel(PathLog.Level.NOTICE, PathLog.thisModule())
+
+
+class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage):
+
+ def getForm(self):
+ return FreeCADGui.PySideUic.loadUi(":/panels/PageOpHelixEdit.ui")
+
+ def getFields(self, obj):
+ PathLog.track()
+ self.obj.Direction = str(self.form.direction.currentText())
+ self.obj.StartSide = str(self.form.startSide.currentText())
+ self.obj.StepOver = self.form.stepOverPercent.value()
+
+ self.updateToolController(obj, self.form.toolController)
+
+ def setFields(self, obj):
+ PathLog.track()
+
+ self.form.stepOverPercent.setValue(self.obj.StepOver)
+ self.selectInComboBox(self.obj.Direction, self.form.direction)
+ self.selectInComboBox(self.obj.StartSide, self.form.startSide)
+
+ self.setupToolController(self.obj, self.form.toolController)
+
+ def getSignalsForUpdate(self, obj):
+ signals = []
+
+ signals.append(self.form.stepOverPercent.editingFinished)
+ signals.append(self.form.direction.currentIndexChanged)
+ signals.append(self.form.startSide.currentIndexChanged)
+ signals.append(self.form.toolController.currentIndexChanged)
+
+ return signals
+
+PathOpGui.SetupOperation('Helix',
+ PathHelix.Create,
+ TaskPanelOpPage,
+ 'Path-Helix',
+ QtCore.QT_TRANSLATE_NOOP("PathHelix", "Helix"),
+ "P, O",
+ QtCore.QT_TRANSLATE_NOOP("PathHelix", "Creates a Path Helix object from a features of a base object"))
+
+FreeCAD.Console.PrintLog("Loading PathHelixGui... done\n")
diff --git a/src/Mod/Path/PathScripts/PathOpGui.py b/src/Mod/Path/PathScripts/PathOpGui.py
index da21b49956..0505cf00d6 100644
--- a/src/Mod/Path/PathScripts/PathOpGui.py
+++ b/src/Mod/Path/PathScripts/PathOpGui.py
@@ -509,6 +509,9 @@ class TaskPanel(object):
for page in self.featurePages:
page.pageUpdateData(obj, prop)
+ def needsFullSpace(self):
+ return True
+
class SelObserver:
def __init__(self, factory):
diff --git a/src/Mod/Path/PathScripts/PathSelection.py b/src/Mod/Path/PathScripts/PathSelection.py
index 6dd97650d5..f12f66605a 100644
--- a/src/Mod/Path/PathScripts/PathSelection.py
+++ b/src/Mod/Path/PathScripts/PathSelection.py
@@ -164,6 +164,7 @@ def select(op):
opsel = {}
opsel['Contour'] = contourselect
opsel['Drilling'] = drillselect
+ opsel['Helix'] = drillselect
opsel['MillFace'] = pocketselect
opsel['Pocket'] = pocketselect
opsel['Profile Edges'] = eselect