# -*- 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 import Path import PathScripts.PathCircularHoleBase as PathCircularHoleBase import PathScripts.PathLog as PathLog import PathScripts.PathOp as PathOp from PathScripts.PathUtils import fmt from PathScripts.PathUtils import findParentJob from PySide import QtCore __title__ = "Path Helix Drill Operation" __author__ = "Lorenz Hüdepohl" __url__ = "http://www.freecadweb.org" __doc__ = "Class and implementation of Helix Drill operation" __contributors__ = "russ4262 (Russell Johnson)" __created__ = "2016" __scriptVersion__ = "1a testing" __lastModified__ = "2019-07-03 11:45 CST" def translate(context, text, disambig=None): return QtCore.QCoreApplication.translate(context, text, disambig) class ObjectHelix(PathCircularHoleBase.ObjectOp): '''Proxy class for Helix operations.''' def circularHoleFeatures(self, obj): '''circularHoleFeatures(obj) ... enable features supported by Helix.''' return PathOp.FeatureStepDown | PathOp.FeatureBaseEdges | PathOp.FeatureBaseFaces | PathOp.FeatureBasePanels def initCircularHoleOperation(self, obj): '''initCircularHoleOperation(obj) ... create helix specific properties.''' obj.addProperty("App::PropertyEnumeration", "Direction", "Helix Drill", translate("PathHelix", "The direction of the circular cuts, ClockWise (CW), or CounterClockWise (CCW)")) obj.Direction = ['CW', 'CCW'] obj.addProperty("App::PropertyEnumeration", "StartSide", "Helix Drill", translate("PathHelix", "Start cutting from the inside or outside")) obj.StartSide = ['Inside', 'Outside'] obj.addProperty("App::PropertyLength", "StepOver", "Helix Drill", translate("PathHelix", "Radius increment (must be smaller than tool diameter)")) obj.addProperty("App::PropertyLength", "StartRadius", "Helix Drill", translate("PathHelix", "Starting Radius")) # Rotation related properties if not hasattr(obj, 'EnableRotation'): obj.addProperty("App::PropertyEnumeration", "EnableRotation", "Rotation", QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable rotation to gain access to pockets/areas not normal to Z axis.")) obj.EnableRotation = ['Off', 'A(x)', 'B(y)', 'A & B'] def circularHoleExecute(self, obj, holes): '''circularHoleExecute(obj, holes) ... generate helix commands for each hole in holes''' PathLog.track() self.commandlist.append(Path.Command('(helix cut operation)')) self.commandlist.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid})) zsafe = max(baseobj.Shape.BoundBox.ZMax for baseobj, features in obj.Base) + obj.ClearanceHeight.Value output = '' output += "G0 Z" + fmt(zsafe) for hole in holes: output += self.helix_cut(obj, hole['x'], hole['y'], hole['r'] / 2, float(obj.StartRadius.Value), (float(obj.StepOver.Value) / 50.0) * self.radius) PathLog.debug(output) def helix_cut(self, obj, x0, y0, r_out, r_in, dr): '''helix_cut(obj, x0, y0, r_out, r_in, dr) ... generate helix commands for specified hole. x0, y0: coordinates of center r_out, r_in: outer and inner radius of the hole dr: step over radius value''' from numpy import ceil, linspace if (obj.StartDepth.Value <= obj.FinalDepth.Value): return "" 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: out += "(full hole mode)\n" r_out = r_out - self.radius 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 obj.StartSide == "Inside": radii = radii[::-1] for r in radii: out += "(radius {0})\n".format(r) out += helix_cut_r(r) return out def opSetDefaultValues(self, obj, job): obj.Direction = "CW" obj.StartSide = "Inside" obj.StepOver = 100 # Initial setting for EnableRotation is taken from Job SetupSheet # User may override on per-operation basis as needed. parentJob = findParentJob(obj) # PathUtils.findParentJob(obj) if hasattr(parentJob.SetupSheet, 'SetupEnableRotation'): obj.EnableRotation = parentJob.SetupSheet.SetupEnableRotation else: obj.EnableRotation = 'Off' def SetupProperties(): setup = [] setup.append("Direction") setup.append("StartSide") setup.append("StepOver") setup.append("EnableRotation") setup.append("StartRadius") return setup def Create(name, obj=None): '''Create(name) ... Creates and returns a Helix operation.''' if obj is None: obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) obj.Proxy = ObjectHelix(obj, name) if obj.Proxy: obj.Proxy.findAllHoles(obj) return obj