diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt
index 701ba44b1..f01eb9b64 100644
--- a/src/Mod/Path/CMakeLists.txt
+++ b/src/Mod/Path/CMakeLists.txt
@@ -53,6 +53,7 @@ SET(PathScripts_SRCS
PathScripts/PathSimpleCopy.py
PathScripts/PathStock.py
PathScripts/PathStop.py
+ PathScripts/PathHelix.py
PathScripts/PathSurface.py
PathScripts/PathToolLenOffset.py
PathScripts/PathToolLibraryManager.py
diff --git a/src/Mod/Path/Gui/Resources/Path.qrc b/src/Mod/Path/Gui/Resources/Path.qrc
index 5be33f8ea..5e701bc5c 100644
--- a/src/Mod/Path/Gui/Resources/Path.qrc
+++ b/src/Mod/Path/Gui/Resources/Path.qrc
@@ -18,6 +18,7 @@
icons/Path-FaceProfile.svg
icons/Path-Face.svg
icons/Path-Heights.svg
+ icons/Path-Helix.svg
icons/Path-Hop.svg
icons/Path-Inspect.svg
icons/Path-Job.svg
diff --git a/src/Mod/Path/Gui/Resources/icons/Path-Helix.svg b/src/Mod/Path/Gui/Resources/icons/Path-Helix.svg
new file mode 100644
index 000000000..48b2ed309
--- /dev/null
+++ b/src/Mod/Path/Gui/Resources/icons/Path-Helix.svg
@@ -0,0 +1,548 @@
+
+
+
+
diff --git a/src/Mod/Path/InitGui.py b/src/Mod/Path/InitGui.py
index b5f020413..22b13882b 100644
--- a/src/Mod/Path/InitGui.py
+++ b/src/Mod/Path/InitGui.py
@@ -65,6 +65,7 @@ class PathWorkbench (Workbench):
from PathScripts import PathCustom
from PathScripts import PathInspect
from PathScripts import PathSimpleCopy
+ from PathScripts import PathHelix
from PathScripts import PathEngrave
from PathScripts import PathSurface
from PathScripts import PathSanity
@@ -80,7 +81,7 @@ class PathWorkbench (Workbench):
projcmdlist = ["Path_Job", "Path_Post", "Path_Inspect", "Path_Sanity"]
toolcmdlist = ["Path_ToolLibraryEdit", "Path_LoadTool"]
prepcmdlist = ["Path_Plane", "Path_Fixture", "Path_ToolLenOffset", "Path_Comment", "Path_Stop", "Path_FaceProfile", "Path_FacePocket", "Path_Custom", "Path_FromShape"]
- twodopcmdlist = ["Path_Contour", "Path_Profile", "Path_Profile_Edges", "Path_Pocket", "Path_Drilling", "Path_Engrave", "Path_MillFace"]
+ twodopcmdlist = ["Path_Contour", "Path_Profile", "Path_Profile_Edges", "Path_Pocket", "Path_Drilling", "Path_Engrave", "Path_MillFace", "Path_Helix"]
threedopcmdlist = ["Path_Surfacing"]
modcmdlist = ["Path_Copy", "Path_CompoundExtended", "Path_Array", "Path_SimpleCopy" ]
dressupcmdlist = ["PathDressup_Dogbone", "PathDressup_DragKnife", "PathDressup_HoldingTags"]
diff --git a/src/Mod/Path/PathScripts/PathHelix.py b/src/Mod/Path/PathScripts/PathHelix.py
new file mode 100644
index 000000000..da2e63a72
--- /dev/null
+++ b/src/Mod/Path/PathScripts/PathHelix.py
@@ -0,0 +1,818 @@
+# -*- coding: utf-8 -*-
+
+#***************************************************************************
+#* *
+#* Copyright (c) 2016 Lorenz Hüdepohl *
+#* *
+#* 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, Path
+if FreeCAD.GuiUp:
+ import FreeCADGui
+ from PySide import QtCore, QtGui
+ from DraftTools import translate
+
+from . import PathUtils
+from .PathUtils import fmt
+
+"""Helix Drill object and FreeCAD command"""
+
+if FreeCAD.GuiUp:
+ try:
+ _encoding = QtGui.QApplication.UnicodeUTF8
+ def translate(context, text, disambig=None):
+ return QtGui.QApplication.translate(context, text, disambig, _encoding)
+ except AttributeError:
+ def translate(context, text, disambig=None):
+ return QtGui.QApplication.translate(context, text, disambig)
+else:
+ def translate(context, text, disambig=None):
+ return text
+
+def z_cylinder(cyl):
+ """ Test if cylinder is aligned to z-Axis"""
+ if cyl.Surface.Axis.x != 0.0:
+ return False
+ if cyl.Surface.Axis.y != 0.0:
+ return False
+ return True
+
+def connected(edge, face):
+ for otheredge in face.Edges:
+ if edge.isSame(otheredge):
+ return True
+ return False
+
+def cylinders_in_selection():
+ from Part import Cylinder
+ selections = FreeCADGui.Selection.getSelectionEx()
+
+ cylinders = []
+
+ for selection in selections:
+ base = selection.Object
+ cylinders.append((base, []))
+ for feature in selection.SubElementNames:
+ subobj = getattr(base.Shape, feature)
+ if subobj.ShapeType =='Face':
+ if isinstance(subobj.Surface, 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, allclose, 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_diamater)
+ 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 not startside 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):
+ import scipy.spatial
+ features = sorted(features,
+ key = lambda feature : getattr(base.Shape, feature).Surface.Radius,
+ reverse = True)
+
+ coordinates = [(cylinder.Surface.Center.x, cylinder.Surface.Center.y) for cylinder in
+ [getattr(base.Shape, feature) for feature in features]]
+
+ tree = scipy.spatial.KDTree(coordinates)
+ seen = {}
+
+ by_centers = {}
+ for n, feature in enumerate(features):
+ if n in seen:
+ continue
+ seen[n] = True
+
+ cylinder = getattr(base.Shape, feature)
+ xc, yc, _ = cylinder.Surface.Center
+ by_centers[xc, yc] = {cylinder.Surface.Radius : feature}
+
+ for coord in tree.query_ball_point((xc, yc), cylinder.Surface.Radius):
+ seen[coord] = True
+ cylinder = getattr(base.Shape, features[coord])
+ by_centers[xc, yc][cylinder.Surface.Radius] = features[coord]
+
+ return by_centers
+
+class ObjectPathHelix(object):
+
+ def __init__(self,obj):
+ # Basic
+ 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.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::PropertyLength", "DeltaR", "Helix Drill",
+ translate("DeltaR", "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"))
+
+ # 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
+
+ obj.Proxy = self
+
+ def __getstate__(self):
+ return None
+
+ def __setstate__(self,state):
+ return None
+
+ def execute(self,obj):
+ from Part import Circle, Cylinder, Plane
+ from math import sqrt
+
+ output = '(helix cut operation'
+ if obj.Comment:
+ output += ', '+ str(obj.Comment)+')\n'
+ else:
+ output += ')\n'
+
+ if obj.Features:
+ if not obj.Active:
+ obj.Path = Path.Path("(helix cut operation inactive)")
+ if obj.ViewObject:
+ obj.ViewObject.Visibility = False
+ return
+
+ toolload = PathUtils.getLastToolLoad(obj)
+
+ 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 = PathUtils.getTool(obj, toolload.ToolNumber)
+
+ 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, 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, r_out=r, r_in=0.0, closed=closed, zsafe=zsafe))
+
+ 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)
+
+ 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
+
+taskpanels = {}
+
+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)
+ taskpanels[0] = 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
+
+ toolLoad = PathUtils.getLastToolLoad(obj)
+ if toolLoad is not None:
+ obj.ToolNumber = toolLoad.ToolNumber
+ tool = PathUtils.getTool(obj, toolLoad.ToolNumber)
+ if tool:
+ # start with 25% overlap
+ obj.DeltaR = tool.Diameter * 0.75
+
+ obj.Active = True
+ obj.Comment = ""
+
+ obj.Direction = "CW"
+ obj.StartSide = "inside"
+
+ 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
+ self.obj = obj
+
+ ui = FreeCADGui.UiLoader()
+ layout = QtGui.QGridLayout()
+
+ headerStyle = "QLabel { font-weight: bold; font-size: large; }"
+ grayed_out = "background-color: #d0d0d0;"
+
+ self.previous_value = {}
+
+ 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 heading(label):
+ heading = QtGui.QLabel(label)
+ heading.setStyleSheet(headerStyle)
+ addWidget(heading)
+
+ 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))
+
+ widget.setText(str(getattr(self.obj, property)))
+ 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)
+
+ heading("Drill parameters")
+ addCheckBox("Active", "Operation is active")
+ tool = PathUtils.getTool(self.obj,self.obj.ToolNumber)
+ 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")])
+
+ heading("Cutting Depths")
+ 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")
+
+ # 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)
+
+ # add
+ widget = QtGui.QWidget()
+ widget.setLayout(layout)
+ self.form = widget
+
+ 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 not feature 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())