Files
create/src/Mod/Path/PathScripts/PathAdaptive.py
2018-09-11 11:02:35 +02:00

413 lines
16 KiB
Python

import PathScripts.PathOp as PathOp
import Path
import FreeCAD
import FreeCADGui
from FreeCAD import Console
import time
import json
import math
import area
from pivy import coin
__doc__ = "Class and implementation of the Adaptive path operation."
def discretize(edge, flipDirection=False):
pts=edge.discretize(Deflection=0.01)
if flipDirection: pts.reverse()
return pts
def IsEqualInXYPlane(e1, e2):
return math.sqrt((e2.x-e1.x)*(e2.x-e1.x) +
(e2.y - e1.y) * (e2.y - e1.y))<0.01
def connectEdges(edges):
''' Makes the list of connected discretized paths '''
# find edge
lastPoint=None
remaining = []
pathArray = []
combined = []
for edge in edges:
p1 = edge.valueAt(edge.FirstParameter)
p2 = edge.valueAt(edge.LastParameter)
duplicate = False
for ex in remaining:
exp1 = ex.valueAt(ex.FirstParameter)
exp2 = ex.valueAt(ex.LastParameter)
if IsEqualInXYPlane(exp1, p1) and IsEqualInXYPlane(exp2, p2):
duplicate = True
if IsEqualInXYPlane(exp1, p2) and IsEqualInXYPlane(exp2, p1):
duplicate = True
if not duplicate:
remaining.append(edge)
newPath=True
while len(remaining)>0:
if newPath:
edge=remaining[0]
p1 = edge.valueAt(edge.FirstParameter)
p2 = edge.valueAt(edge.LastParameter)
if len(combined)>0: pathArray.append(combined)
combined = []
combined.append(discretize(edge))
remaining.remove(edge)
lastPoint=p2
newPath=False
anyMatch=False
for e in remaining:
p1 = e.valueAt(e.FirstParameter)
p2 = e.valueAt(e.LastParameter)
if IsEqualInXYPlane(lastPoint,p1):
combined.append(discretize(e))
remaining.remove(e)
lastPoint=p2
anyMatch=True
break
elif IsEqualInXYPlane(lastPoint,p2):
combined.append(discretize(e,True))
remaining.remove(e)
lastPoint=p1
anyMatch=True
break
if not anyMatch:
newPath=True
#make sure last path is appended
if len(combined)>0: pathArray.append(combined)
combined = []
return pathArray
def convertTo2d(pathArray):
output = []
for path in pathArray:
pth2 = []
for edge in path:
for pt in edge:
pth2.append([pt[0],pt[1]])
output.append(pth2)
return output
sceneGraph = None
scenePathNodes = [] #for scene cleanup aftewards
topZ = 10
def sceneDrawPath(path, color=(0, 0, 1)):
global sceneGraph
global scenePathNodes
coPoint = coin.SoCoordinate3()
pts = []
for pt in path:
pts.append([pt[0], pt[1], topZ])
coPoint.point.setValues(0, len(pts), pts)
ma = coin.SoBaseColor()
ma.rgb = color
li = coin.SoLineSet()
li.numVertices.setValue(len(pts))
pathNode = coin.SoSeparator()
pathNode.addChild(coPoint)
pathNode.addChild(ma)
pathNode.addChild(li)
sceneGraph.addChild(pathNode)
scenePathNodes.append(pathNode) #for scene cleanup afterwards
def sceneClean():
global scenePathNodes
for n in scenePathNodes:
sceneGraph.removeChild(n)
del scenePathNodes[:]
def GenerateGCode(op,obj,adaptiveResults, helixDiameter):
if len(adaptiveResults)==0 or len(adaptiveResults[0]["AdaptivePaths"])==0:
return
minLiftDistance = op.tool.Diameter
p1 = adaptiveResults[0]["HelixCenterPoint"]
p2 = adaptiveResults[0]["StartPoint"]
helixRadius =math.sqrt((p1[0]-p2[0]) * (p1[0]-p2[0]) + (p1[1]-p2[1]) * (p1[1]-p2[1]))
stepDown = obj.StepDown.Value
passStartDepth=obj.StartDepth.Value
if stepDown<0.1 : stepDown=0.1
length = 2*math.pi * helixRadius
if obj.HelixAngle<1: obj.HelixAngle=1
helixAngleRad = math.pi * obj.HelixAngle/180.0
depthPerOneCircle=length * math.tan(helixAngleRad)
stepUp = obj.LiftDistance.Value
if stepUp<0:
stepUp=0
lx=adaptiveResults[0]["HelixCenterPoint"][0]
ly=adaptiveResults[0]["HelixCenterPoint"][1]
step=0
while passStartDepth>obj.FinalDepth.Value and step<1000:
step=step+1
passEndDepth=passStartDepth-stepDown
if passEndDepth<obj.FinalDepth.Value: passEndDepth=obj.FinalDepth.Value
for region in adaptiveResults:
startAngle = math.atan2(region["StartPoint"][1] - region["HelixCenterPoint"][1], region["StartPoint"][0] - region["HelixCenterPoint"][0])
lx=region["HelixCenterPoint"][0]
ly=region["HelixCenterPoint"][1]
r = helixRadius - 0.01
#helix ramp
passDepth = (passStartDepth - passEndDepth)
maxfi = passDepth / depthPerOneCircle * 2 * math.pi
fi = 0
offsetFi =-maxfi + startAngle-math.pi/16
helixStart = [region["HelixCenterPoint"][0] + r * math.cos(offsetFi), region["HelixCenterPoint"][1] + r * math.sin(offsetFi)]
op.commandlist.append(Path.Command("(helix to depth: %f)"%passEndDepth))
#if step == 1:
#rapid move to start point
op.commandlist.append(Path.Command(
"G0", {"X": helixStart[0], "Y": helixStart[1], "Z": obj.ClearanceHeight.Value}))
#rapid move to safe height
op.commandlist.append(Path.Command(
"G0", {"X": helixStart[0], "Y": helixStart[1], "Z": obj.SafeHeight.Value}))
op.commandlist.append(Path.Command("G1", {
"X": helixStart[0], "Y": helixStart[1], "Z": passStartDepth, "F": op.vertFeed}))
while fi<maxfi:
x = region["HelixCenterPoint"][0] + r * math.cos(fi+offsetFi)
y = region["HelixCenterPoint"][1] + r * math.sin(fi+offsetFi)
z = passStartDepth - fi / maxfi * (passStartDepth - passEndDepth)
op.commandlist.append(Path.Command("G1", { "X": x, "Y":y, "Z":z, "F": op.vertFeed}))
lx=x
ly=y
fi=fi+math.pi/16
op.commandlist.append(Path.Command("(adaptive - depth: %f)"%passEndDepth))
#add adaptive paths
for pth in region["AdaptivePaths"]:
motionType = pth[0] #[0] contains motion type
for pt in pth[1]: #[1] contains list of points
x=pt[0]
y =pt[1]
dist=math.sqrt((x-lx)*(x-lx) + (y-ly)*(y-ly))
if motionType == area.AdaptiveMotionType.Cutting:
op.commandlist.append(Path.Command("G1", { "X": x, "Y":y, "Z":passEndDepth, "F": op.horizFeed}))
elif motionType == area.AdaptiveMotionType.LinkClear:
if dist > minLiftDistance:
if lx!=x or ly!=y:
op.commandlist.append(Path.Command("G0", { "X": lx, "Y":ly, "Z":passEndDepth+stepUp}))
op.commandlist.append(Path.Command("G0", { "X": x, "Y":y, "Z":passEndDepth+stepUp}))
elif motionType == area.AdaptiveMotionType.LinkNotClear:
if lx!=x or ly!=y:
op.commandlist.append(Path.Command("G0", { "X": lx, "Y":ly, "Z":obj.ClearanceHeight.Value}))
op.commandlist.append(Path.Command("G0", { "X": x, "Y":y, "Z":obj.ClearanceHeight.Value}))
elif motionType == area.AdaptiveMotionType.LinkClearAtPrevPass:
if lx!=x or ly!=y:
op.commandlist.append(Path.Command("G0", { "X": lx, "Y":ly, "Z":passStartDepth+stepUp}))
op.commandlist.append(Path.Command("G0", { "X": x, "Y":y, "Z":passStartDepth+stepUp}))
lx=x
ly=y
passStartDepth=passEndDepth
#return to safe height in this Z pass
op.commandlist.append(Path.Command("G0", { "X": lx, "Y":ly, "Z":obj.ClearanceHeight.Value}))
op.commandlist.append(Path.Command("G0", { "X": lx, "Y":ly, "Z":obj.ClearanceHeight.Value}))
def Execute(op,obj):
global sceneGraph
global topZ
sceneGraph = FreeCADGui.ActiveDocument.ActiveView.getSceneGraph()
Console.PrintMessage("*** Adaptive toolpath processing started...\n")
#hide old toolpaths during recalculation
obj.Path = Path.Path("(calculating...)")
#store old visibility state
job = op.getJob(obj)
oldObjVisibility = obj.ViewObject.Visibility
oldJobVisibility = job.ViewObject.Visibility
obj.ViewObject.Visibility = False
job.ViewObject.Visibility = False
FreeCADGui.updateGui()
try:
Console.PrintMessage("Tool diam: %f \n"%op.tool.Diameter)
helixDiameter = min(op.tool.Diameter,1000.0 if obj.HelixDiameterLimit.Value==0.0 else obj.HelixDiameterLimit.Value )
nestingLimit=0
topZ=op.stock.Shape.BoundBox.ZMax
opType = area.AdaptiveOperationType.Clearing
obj.Stopped = False
obj.StopProcessing = False
if obj.Tolerance<0.001: obj.Tolerance=0.001
edges=[]
for base, subs in obj.Base:
for sub in subs:
shape=base.Shape.getElement(sub)
for edge in shape.Edges:
edges.append(edge)
pathArray=connectEdges(edges)
if obj.OperationType == "Clearing":
if obj.Side == "Outside":
stockBB = op.stock.Shape.BoundBox
v=[]
v.append(FreeCAD.Vector(stockBB.XMin,stockBB.YMin,0))
v.append(FreeCAD.Vector(stockBB.XMax,stockBB.YMin,0))
v.append(FreeCAD.Vector(stockBB.XMax,stockBB.YMax,0))
v.append(FreeCAD.Vector(stockBB.XMin,stockBB.YMax,0))
v.append(FreeCAD.Vector(stockBB.XMin,stockBB.YMin,0))
pathArray.append([v])
if not obj.ProcessHoles: nestingLimit = 2
elif not obj.ProcessHoles: nestingLimit = 1
opType = area.AdaptiveOperationType.Clearing
else: # profiling
if obj.Side == "Outside":
opType = area.AdaptiveOperationType.ProfilingOutside
else:
opType = area.AdaptiveOperationType.ProfilingInside
if not obj.ProcessHoles: nestingLimit = 1
path2d = convertTo2d(pathArray)
# put here all properties that influence calculation of adaptive base paths,
inputStateObject = {
"tool": op.tool.Diameter,
"tolerance": obj.Tolerance,
"geometry" : path2d,
"stepover" :obj.StepOver,
"effectiveHelixDiameter": helixDiameter,
"operationType": obj.OperationType,
"side": obj.Side,
"processHoles": obj.ProcessHoles
}
inputStateChanged=False
adaptiveResults=None
if obj.AdaptiveOutputState !=None and obj.AdaptiveOutputState != "":
adaptiveResults = obj.AdaptiveOutputState
if json.dumps(obj.AdaptiveInputState) != json.dumps(inputStateObject):
inputStateChanged=True
adaptiveResults=None
# progress callback fn, if return true it will stop processing
def progressFn(tpaths):
for path in tpaths: #path[0] contains the MotionType,#path[1] contains list of points
sceneDrawPath(path[1])
FreeCADGui.updateGui()
return obj.StopProcessing
start=time.time()
if inputStateChanged or adaptiveResults==None:
a2d = area.Adaptive2d()
a2d.stepOverFactor = 0.01*obj.StepOver
a2d.toolDiameter = op.tool.Diameter
a2d.helixRampDiameter = helixDiameter
a2d.tolerance = obj.Tolerance
a2d.opType = opType
a2d.polyTreeNestingLimit = nestingLimit
#EXECUTE
results = a2d.Execute(path2d,progressFn)
#need to convert results to python object to be JSON serializable
adaptiveResults = []
for result in results:
adaptiveResults.append({
"HelixCenterPoint": result.HelixCenterPoint,
"StartPoint": result.StartPoint,
"AdaptivePaths": result.AdaptivePaths,
"ReturnMotionType": result.ReturnMotionType })
GenerateGCode(op,obj,adaptiveResults,helixDiameter)
if not obj.StopProcessing:
Console.PrintMessage("*** Done. Elapsed: %f sec\n\n" %(time.time()-start))
obj.AdaptiveOutputState = adaptiveResults
obj.AdaptiveInputState=inputStateObject
else:
Console.PrintMessage("*** Processing cancelled (after: %f sec).\n\n" %(time.time()-start))
finally:
obj.ViewObject.Visibility = oldObjVisibility
job.ViewObject.Visibility = oldJobVisibility
sceneClean()
class PathAdaptive(PathOp.ObjectOp):
def opFeatures(self, obj):
'''opFeatures(obj) ... returns the OR'ed list of features used and supported by the operation.
The default implementation returns "FeatureTool | FeatureDeptsh | FeatureHeights | FeatureStartPoint"
Should be overwritten by subclasses.'''
return PathOp.FeatureTool | PathOp.FeatureBaseEdges | PathOp.FeatureDepths | PathOp.FeatureStepDown | PathOp.FeatureHeights | PathOp.FeatureBaseGeometry
def initOperation(self, obj):
'''initOperation(obj) ... implement to create additional properties.
Should be overwritten by subclasses.'''
obj.addProperty("App::PropertyEnumeration", "Side", "Adaptive", "Side of selected faces that tool should cut")
obj.Side = ['Outside', 'Inside'] # side of profile that cutter is on in relation to direction of profile
obj.addProperty("App::PropertyEnumeration", "OperationType", "Adaptive", "Type of adaptive operation")
obj.OperationType = ['Clearing', 'Profiling'] # side of profile that cutter is on in relation to direction of profile
obj.addProperty("App::PropertyFloat", "Tolerance", "Adaptive", "Influences accuracy and performance")
obj.addProperty("App::PropertyPercent", "StepOver", "Adaptive", "Percent of cutter diameter to step over on each pass")
obj.addProperty("App::PropertyDistance", "LiftDistance", "Adaptive", "Lift distance for rapid moves")
obj.addProperty("App::PropertyBool", "ProcessHoles", "Adaptive","Process holes as well as the face outline")
obj.addProperty("App::PropertyBool", "Stopped",
"Adaptive", "Stop processing")
obj.setEditorMode('Stopped', 2) #hide this property
obj.addProperty("App::PropertyBool", "StopProcessing",
"Adaptive", "Stop processing")
obj.setEditorMode('StopProcessing', 2) # hide this property
obj.addProperty("App::PropertyPythonObject", "AdaptiveInputState",
"Adaptive", "Internal input state")
obj.addProperty("App::PropertyPythonObject", "AdaptiveOutputState",
"Adaptive", "Internal output state")
obj.setEditorMode('AdaptiveInputState', 2) #hide this property
obj.setEditorMode('AdaptiveOutputState', 2) #hide this property
obj.addProperty("App::PropertyAngle", "HelixAngle", "Adaptive", "Helix ramp entry angle (degrees)")
obj.addProperty("App::PropertyLength", "HelixDiameterLimit", "Adaptive", "Limit helix entry diameter, if limit larger than tool diameter or 0, tool diameter is used")
def opSetDefaultValues(self, obj):
obj.Side="Inside"
obj.OperationType = "Clearing"
obj.Tolerance = 0.1
obj.StepOver = 20
obj.LiftDistance=1.0
obj.ProcessHoles = True
obj.Stopped = False
obj.StopProcessing = False
obj.HelixAngle = 5
obj.HelixDiameterLimit = 0.0
obj.AdaptiveInputState =""
obj.AdaptiveOutputState = ""
def opExecute(self, obj):
'''opExecute(obj) ... called whenever the receiver needs to be recalculated.
See documentation of execute() for a list of base functionality provided.
Should be overwritten by subclasses.'''
Execute(self,obj)
def Create(name):
'''Create(name) ... Creates and returns a Pocket operation.'''
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
proxy = PathAdaptive(obj)
return obj