Rebased Helix op on PathOp framework, also it's UI.

This commit is contained in:
Markus Lampert
2017-08-08 16:35:37 -07:00
committed by Yorik van Havre
parent 355075f7c3
commit b6c31bedab
12 changed files with 765 additions and 870 deletions

View File

@@ -0,0 +1,224 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * *
# * Copyright (c) 2017 sliptonic <shopinthewoods@gmail.com> *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
import FreeCAD
import 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

View File

@@ -0,0 +1,152 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * *
# * Copyright (c) 2017 sliptonic <shopinthewoods@gmail.com> *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
import FreeCAD
import FreeCADGui
import PathScripts.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)

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * *
# * Copyright (c) 2017 sliptonic <shopinthewoods@gmail.com> *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
import FreeCAD
import FreeCADGui
import PathScripts.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")

View File

@@ -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):

View File

@@ -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