497 lines
20 KiB
Python
497 lines
20 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
#***************************************************************************
|
|
#* *
|
|
#* Copyright (c) 2016 Lorenz Hüdepohl <dev@stellardeath.org> *
|
|
#* *
|
|
#* 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
|
|
from PySide import QtCore,QtGui
|
|
from PathScripts import PathUtils
|
|
from PathUtils import fmt
|
|
|
|
"""Helix Drill object and FreeCAD command"""
|
|
|
|
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)
|
|
|
|
def hollow_cylinder(cyl):
|
|
"""Test if this is a hollow cylinder"""
|
|
from Part import Circle
|
|
circle1 = None
|
|
line = None
|
|
for edge in cyl.Edges:
|
|
if isinstance(edge.Curve, Circle):
|
|
if circle1 is None:
|
|
circle1 = edge
|
|
else:
|
|
circle2 = edge
|
|
else:
|
|
line = edge
|
|
center = (circle1.Curve.Center + circle2.Curve.Center).scale(0.5, 0.5, 0.5)
|
|
p = (circle1.valueAt(circle1.ParameterRange[0]) + circle2.valueAt(circle1.ParameterRange[0])).scale(0.5, 0.5, 0.5)
|
|
to_outside = (p - center).normalize()
|
|
u, v = cyl.Surface.parameter(p)
|
|
normal = cyl.normalAt(u, v).normalize()
|
|
|
|
cos_a = to_outside.dot(normal)
|
|
|
|
if cos_a > 1.0 - 1e-12:
|
|
return False
|
|
elif cos_a < -1.0 + 1e-12:
|
|
return True
|
|
else:
|
|
raise Exception("Strange cylinder encountered, cannot determine if it is hollow or not")
|
|
|
|
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 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
|
|
|
|
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))))
|
|
|
|
x0, y0 = center
|
|
nz = int(ceil(2*(zmax - zmin)/dz))
|
|
dz = (zmax - zmin) / nz
|
|
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)
|
|
for i in range(1,nz+2):
|
|
out += arc(x0-r, y0, i=-r, j=0.0, z = max(zmax - (i - 0.5) * dz, zmin), f=hfeed)
|
|
out += arc(x0+r, y0, i=r, j=0.0, z = max(zmax - i * dz, 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 < 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
|
|
|
|
class ObjectPathHelix:
|
|
|
|
def __init__(self,obj):
|
|
|
|
# Basic
|
|
obj.addProperty("App::PropertyLinkSub","Base","Path",translate("Parent Object","The base geometry of this toolpath"))
|
|
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"))
|
|
obj.addProperty("App::PropertyBool", "Recursive", "Helix Drill", translate("Recursive", "If True, drill holes also in any subsequent holes at the bottom of holes that are not fully through"))
|
|
|
|
# Depth Properties
|
|
obj.addProperty("App::PropertyDistance", "ClearanceHeight", "Depth", translate("Clearance Height","Distance above edge to which to retract the tool"))
|
|
obj.addProperty("App::PropertyLength", "StepDown", "Depth", translate("StepDown","Incremental Step Down of Tool"))
|
|
obj.addProperty("App::PropertyBool","UseStartDepth","Depth",translate("Use Start Depth","Set to True to manually specify a start depth"))
|
|
obj.addProperty("App::PropertyDistance", "StartDepth", "Depth", translate("Start Depth","Starting Depth of Tool - first cut depth in Z"))
|
|
obj.addProperty("App::PropertyBool","UseFinalDepth","Depth", translate("Use Final Depth","Set to True to manually specify a final depth"))
|
|
obj.addProperty("App::PropertyDistance", "FinalDepth", "Depth", translate("Final Depth","Final Depth of Tool - lowest value in Z"))
|
|
obj.addProperty("App::PropertyDistance", "ThroughDepth", "Depth", translate("Through Depth","Add this amount of additional cutting depth to open holes, "
|
|
"only used if UseFinalDepth is False"))
|
|
|
|
# Feed Properties
|
|
obj.addProperty("App::PropertySpeed", "VertFeed", "Feed", translate("Vert Feed","Feed rate for vertical moves"))
|
|
obj.addProperty("App::PropertySpeed", "HorizFeed", "Feed", translate("Horiz Feed","Feed rate for horizontal moves"))
|
|
|
|
# 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
|
|
if obj.Base:
|
|
if not obj.Active:
|
|
obj.Path = Path.Path("(helix cut operation inactive)")
|
|
obj.ViewObject.Visibility = False
|
|
return
|
|
|
|
if not len(obj.InList) > 0:
|
|
FreeCAD.Console.PrintError("PathHelix: Operation is not part of a project\n")
|
|
obj.Path = Path.Path("(helix cut operation not part of any project)")
|
|
obj.ViewObject.Visibility = False
|
|
return
|
|
|
|
project = obj.InList[0]
|
|
obj.ToolNumber = int(PathUtils.changeTool(obj,project))
|
|
tool = PathUtils.getTool(obj,obj.ToolNumber)
|
|
|
|
if not tool:
|
|
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
|
|
|
|
def connected_cylinders(base, edge):
|
|
cylinders = []
|
|
for face in base.Shape.Faces:
|
|
if isinstance(face.Surface, Cylinder):
|
|
if connected(edge, face):
|
|
if z_cylinder(face):
|
|
cylinders.append((base, face))
|
|
return cylinders
|
|
|
|
cylinders = []
|
|
|
|
for base, feature in obj.Features:
|
|
subobj = getattr(base.Shape, feature)
|
|
if subobj.ShapeType =='Face':
|
|
if isinstance(subobj.Surface, Cylinder):
|
|
if z_cylinder(subobj):
|
|
cylinders.append((base, subobj))
|
|
else:
|
|
# brute force triple-loop as FreeCAD does not expose
|
|
# any topology information...
|
|
for edge in subobj.Edges:
|
|
cylinders.extend(filter(lambda b_c: hollow_cylinder(b_c[1]), (connected_cylinders(base, edge))))
|
|
|
|
if subobj.ShapeType == 'Edge':
|
|
cylinders.extend(connected_cylinders(base, subobj))
|
|
|
|
output = '(helix cut operation'
|
|
if obj.Comment:
|
|
output += ', '+ str(obj.Comment)+')\n'
|
|
else:
|
|
output += ')\n'
|
|
|
|
output += "G0 Z" + fmt(obj.Base[0].Shape.BoundBox.ZMax + float(obj.ClearanceHeight))
|
|
|
|
drill_jobs = []
|
|
|
|
for base, cylinder in cylinders:
|
|
xc, yc, zc = cylinder.Surface.Center
|
|
if obj.UseStartDepth:
|
|
zmax = obj.StartDepth.Value
|
|
else:
|
|
zmax = cylinder.BoundBox.ZMax
|
|
|
|
if obj.Recursive:
|
|
cur_z = zmax
|
|
jobs = []
|
|
|
|
while cylinder:
|
|
# 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
|
|
print cur_z, dz, r
|
|
|
|
if dz < 0:
|
|
# This is a closed hole if the face connecting 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))
|
|
|
|
jobs.append(dict(xc=xc, yc=yc, zmin=next_z, zmax=cur_z, 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.PrintWarning("PathHelix: Encountered cylinder with zero height\n")
|
|
break
|
|
|
|
cur_z = next_z
|
|
cylinder = None
|
|
faces = []
|
|
for face in base.Shape.Faces:
|
|
if connected(other_edge, face):
|
|
if isinstance(face.Surface, Plane):
|
|
faces.append(face)
|
|
face, = faces
|
|
for edge in face.Edges:
|
|
if not edge.isSame(other_edge):
|
|
for base, other_cylinder in connected_cylinders(base, edge):
|
|
if other_cylinder.Surface.Center.x == xc and other_cylinder.Surface.Center.y == yc and other_cylinder.Surface.Radius < r:
|
|
cylinder = other_cylinder
|
|
break
|
|
|
|
if obj.UseFinalDepth:
|
|
jobs[-1]["zmin"] = obj.FinalDepth.Value
|
|
else:
|
|
if not jobs[-1]["closed"]:
|
|
jobs[-1]["zmin"] -= obj.ThroughDepth.Value
|
|
|
|
drill_jobs.extend(jobs)
|
|
else:
|
|
if obj.UseFinalDepth:
|
|
zmin = obj.FinalDepth.Value
|
|
else:
|
|
zmin = cylinder.BoundBox.ZMin - obj.ThroughDepth.Value
|
|
drill_jobs.append(dict(xc=xc, yc=yc, zmin=zmin, zmax=zmax, r_out=cylinder.Surface.Radius, r_in=0.0))
|
|
|
|
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["zmax"] + obj.ClearanceHeight.Value, tool.Diameter,
|
|
obj.VertFeed.Value, obj.HorizFeed.Value, obj.Direction, obj.StartSide)
|
|
output += '\n'
|
|
|
|
obj.Path = Path.Path(output)
|
|
if obj.ViewObject:
|
|
obj.ViewObject.Visibility = True
|
|
|
|
|
|
class ViewProviderPathHelix:
|
|
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 __getstate__(self):
|
|
return None
|
|
|
|
def __setstate__(self,state):
|
|
return None
|
|
|
|
|
|
class CommandPathHelix:
|
|
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):
|
|
return not FreeCAD.ActiveDocument is None
|
|
|
|
def Activated(self):
|
|
import FreeCADGui
|
|
import Path
|
|
from PathScripts import PathUtils, PathHelix
|
|
|
|
selection = FreeCADGui.Selection.getSelectionEx()
|
|
|
|
if not len(selection) == 1:
|
|
FreeCAD.Console.PrintError("Only considering first object for PathHelix!\n")
|
|
selection = selection[0]
|
|
|
|
if not len(selection.SubElementNames) > 0:
|
|
FreeCAD.Console.PrintError("Select a face or circles to create helix cuts\n")
|
|
|
|
# register the transaction for the undo stack
|
|
try:
|
|
FreeCAD.ActiveDocument.openTransaction(translate("PathHelix","Create a helix cut"))
|
|
FreeCADGui.addModule("PathScripts.PathHelix")
|
|
|
|
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython","PathHelix")
|
|
PathHelix.ObjectPathHelix(obj)
|
|
PathHelix.ViewProviderPathHelix(obj.ViewObject)
|
|
|
|
obj.Base = selection.Object
|
|
obj.Features = [(selection.Object, subobj) for subobj in selection.SubElementNames]
|
|
obj.DeltaR = 1.0
|
|
|
|
project = PathUtils.addToProject(obj)
|
|
tl = PathUtils.changeTool(obj,project)
|
|
if tl:
|
|
obj.ToolNumber = tl
|
|
tool = PathUtils.getTool(obj,obj.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.ClearanceHeight = 10.0
|
|
obj.StepDown = 1.0
|
|
obj.UseStartDepth = False
|
|
obj.StartDepth = 1.0
|
|
obj.UseFinalDepth = False
|
|
obj.FinalDepth = 0.0
|
|
obj.ThroughDepth = 0.0
|
|
obj.Recursive = True
|
|
|
|
obj.VertFeed = 0.0
|
|
obj.HorizFeed = 0.0
|
|
|
|
# commit
|
|
FreeCAD.ActiveDocument.commitTransaction()
|
|
|
|
except:
|
|
FreeCAD.ActiveDocument.abortTransaction()
|
|
raise
|
|
|
|
FreeCAD.ActiveDocument.recompute()
|
|
|
|
if FreeCAD.GuiUp:
|
|
import FreeCADGui
|
|
FreeCADGui.addCommand('Path_Helix',CommandPathHelix())
|