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