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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + 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())