implemented the same way as in other path area operations reusing the PathUtils.depth_params class
445 lines
19 KiB
Python
445 lines
19 KiB
Python
# /**************************************************************************
|
|
# * Copyright (c) Kresimir Tusek (kresimir.tusek@gmail.com) 2018 *
|
|
# * This file is part of the FreeCAD CAx development system. *
|
|
# * *
|
|
# * This library is free software; you can redistribute it and/or *
|
|
# * modify it under the terms of the GNU Library General Public *
|
|
# * License as published by the Free Software Foundation; either *
|
|
# * version 2 of the License, or (at your option) any later version. *
|
|
# * *
|
|
# * This library 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 library; see the file COPYING.LIB. If not, *
|
|
# * write to the Free Software Foundation, Inc., 59 Temple Place, *
|
|
# * Suite 330, Boston, MA 02111-1307, USA *
|
|
# * *
|
|
# ***************************************************************************/
|
|
|
|
|
|
|
|
import PathScripts.PathOp as PathOp
|
|
import PathScripts.PathUtils as PathUtils
|
|
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 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
|
|
helixRadius=0
|
|
for region in adaptiveResults:
|
|
p1 = region["HelixCenterPoint"]
|
|
p2 = region["StartPoint"]
|
|
r =math.sqrt((p1[0]-p2[0]) * (p1[0]-p2[0]) + (p1[1]-p2[1]) * (p1[1]-p2[1]))
|
|
if r>helixRadius: helixRadius=r
|
|
|
|
stepDown = obj.StepDown.Value
|
|
passStartDepth=obj.StartDepth.Value
|
|
if stepDown<0.1 : stepDown=0.1
|
|
length = 2*math.pi * helixRadius
|
|
if float(obj.HelixAngle)<1: obj.HelixAngle=1
|
|
helixAngleRad = math.pi * float(obj.HelixAngle)/180.0
|
|
depthPerOneCircle=length * math.tan(helixAngleRad)
|
|
stepUp = obj.LiftDistance.Value
|
|
if stepUp<0:
|
|
stepUp=0
|
|
|
|
|
|
finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0
|
|
if finish_step>stepDown: finish_step = stepDown
|
|
|
|
depth_params = PathUtils.depth_params(
|
|
clearance_height=obj.ClearanceHeight.Value,
|
|
safe_height=obj.SafeHeight.Value,
|
|
start_depth=obj.StartDepth.Value,
|
|
step_down=stepDown,
|
|
z_finish_step=finish_step,
|
|
final_depth=obj.FinalDepth.Value,
|
|
user_depths=None)
|
|
|
|
|
|
|
|
lx=adaptiveResults[0]["HelixCenterPoint"][0]
|
|
ly=adaptiveResults[0]["HelixCenterPoint"][1]
|
|
lz=passStartDepth
|
|
step=0
|
|
for passEndDepth in depth_params.data:
|
|
step=step+1
|
|
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]
|
|
|
|
passDepth = (passStartDepth - passEndDepth)
|
|
|
|
p1 = region["HelixCenterPoint"]
|
|
p2 = region["StartPoint"]
|
|
helixRadius =math.sqrt((p1[0]-p2[0]) * (p1[0]-p2[0]) + (p1[1]-p2[1]) * (p1[1]-p2[1]))
|
|
|
|
#helix ramp
|
|
if helixRadius>0.0001:
|
|
r = helixRadius - 0.01
|
|
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))
|
|
|
|
#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
|
|
# one more circle at target depth to make sure center is cleared
|
|
maxfi=maxfi+2*math.pi
|
|
while fi<maxfi:
|
|
x = region["HelixCenterPoint"][0] + r * math.cos(fi+offsetFi)
|
|
y = region["HelixCenterPoint"][1] + r * math.sin(fi+offsetFi)
|
|
z = passEndDepth
|
|
op.commandlist.append(Path.Command("G1", { "X": x, "Y":y, "Z":z, "F": op.horizFeed}))
|
|
lx=x
|
|
ly=y
|
|
fi=fi+math.pi/16
|
|
else: # no helix entry
|
|
op.commandlist.append(Path.Command(
|
|
"G0", {"X": region["StartPoint"][0], "Y": region["StartPoint"][1], "Z": obj.ClearanceHeight.Value}))
|
|
op.commandlist.append(Path.Command("G1", {
|
|
"X":region["StartPoint"][0], "Y": region["StartPoint"][1], "Z": passEndDepth,"F": op.vertFeed}))
|
|
|
|
lz=passEndDepth
|
|
z=obj.ClearanceHeight.Value
|
|
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:
|
|
z=passEndDepth
|
|
if z!=lz: op.commandlist.append(Path.Command("G1", { "Z":z,"F": op.vertFeed}))
|
|
op.commandlist.append(Path.Command("G1", { "X": x, "Y":y, "F": op.horizFeed}))
|
|
elif motionType == area.AdaptiveMotionType.LinkClear:
|
|
z=passEndDepth+stepUp
|
|
if z!=lz: op.commandlist.append(Path.Command("G0", { "Z":z}))
|
|
op.commandlist.append(Path.Command("G0", { "X": x, "Y":y}))
|
|
elif motionType == area.AdaptiveMotionType.LinkNotClear:
|
|
z=obj.ClearanceHeight.Value
|
|
if z!=lz: op.commandlist.append(Path.Command("G0", { "Z":z}))
|
|
op.commandlist.append(Path.Command("G0", { "X": x, "Y":y}))
|
|
# 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
|
|
lz=z
|
|
#return to safe height in this Z pass
|
|
z=obj.ClearanceHeight.Value
|
|
if z!=lz: op.commandlist.append(Path.Command("G0", { "Z":z}))
|
|
lz=z
|
|
passStartDepth=passEndDepth
|
|
#return to safe height in this Z pass
|
|
z=obj.ClearanceHeight.Value
|
|
if z!=lz: op.commandlist.append(Path.Command("G0", { "Z":z}))
|
|
lz=z
|
|
z=obj.ClearanceHeight.Value
|
|
if z!=lz: op.commandlist.append(Path.Command("G0", { "Z":z}))
|
|
lz=z
|
|
|
|
|
|
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:
|
|
helixDiameter =obj.HelixDiameterLimit.Value
|
|
topZ=op.stock.Shape.BoundBox.ZMax
|
|
obj.Stopped = False
|
|
obj.StopProcessing = False
|
|
if obj.Tolerance<0.001: obj.Tolerance=0.001
|
|
|
|
pathArray=[]
|
|
for base, subs in obj.Base:
|
|
for sub in subs:
|
|
shape=base.Shape.getElement(sub)
|
|
for edge in shape.Edges:
|
|
pathArray.append([discretize(edge)])
|
|
|
|
#pathArray=connectEdges(edges)
|
|
path2d = convertTo2d(pathArray)
|
|
|
|
stockPaths = []
|
|
if op.stock.StockType == "CreateCylinder":
|
|
stockPaths.append([discretize(op.stock.Shape.Edges[0])])
|
|
else:
|
|
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))
|
|
stockPaths.append([v])
|
|
|
|
stockPath2d = convertTo2d(stockPaths)
|
|
|
|
opType = area.AdaptiveOperationType.ClearingInside
|
|
if obj.OperationType == "Clearing":
|
|
if obj.Side == "Outside":
|
|
opType = area.AdaptiveOperationType.ClearingOutside
|
|
else:
|
|
opType = area.AdaptiveOperationType.ClearingInside
|
|
else: # profiling
|
|
if obj.Side == "Outside":
|
|
opType = area.AdaptiveOperationType.ProfilingOutside
|
|
else:
|
|
opType = area.AdaptiveOperationType.ProfilingInside
|
|
|
|
|
|
keepToolDownRatio=3.0
|
|
if hasattr(obj, 'KeepToolDownRatio'): keepToolDownRatio = float(obj.KeepToolDownRatio)
|
|
# put here all properties that influence calculation of adaptive base paths,
|
|
|
|
inputStateObject = {
|
|
"tool": float(op.tool.Diameter),
|
|
"tolerance": float(obj.Tolerance),
|
|
"geometry" : path2d,
|
|
"stockGeometry": stockPath2d,
|
|
"stepover" : float(obj.StepOver),
|
|
"effectiveHelixDiameter": float(helixDiameter),
|
|
"operationType": obj.OperationType,
|
|
"side": obj.Side,
|
|
"forceInsideOut" : obj.ForceInsideOut,
|
|
"keepToolDownRatio": keepToolDownRatio,
|
|
"stockToLeave": float(obj.StockToLeave)
|
|
}
|
|
|
|
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
|
|
if path[0] == area.AdaptiveMotionType.Cutting:
|
|
sceneDrawPath(path[1],(0,0,1))
|
|
else:
|
|
sceneDrawPath(path[1],(1,0,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 = float(op.tool.Diameter)
|
|
a2d.helixRampDiameter = helixDiameter
|
|
a2d.keepToolDownDistRatio = keepToolDownRatio
|
|
a2d.stockToLeave =float(obj.StockToLeave)
|
|
a2d.tolerance = float(obj.Tolerance)
|
|
a2d.forceInsideOut = obj.ForceInsideOut
|
|
a2d.opType = opType
|
|
#EXECUTE
|
|
results = a2d.Execute(stockPath2d,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.FeatureFinishDepth | 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::PropertyDistance", "KeepToolDownRatio", "Adaptive", "Max length of keep tool down path compared to direct distance between points")
|
|
obj.addProperty("App::PropertyDistance", "StockToLeave", "Adaptive", "How much stock to leave (i.e. for finishing operation)")
|
|
# obj.addProperty("App::PropertyBool", "ProcessHoles", "Adaptive","Process holes as well as the face outline")
|
|
|
|
obj.addProperty("App::PropertyBool", "ForceInsideOut", "Adaptive","Force plunging into material inside and clearing towards the edges")
|
|
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, job):
|
|
obj.Side="Inside"
|
|
obj.OperationType = "Clearing"
|
|
obj.Tolerance = 0.1
|
|
obj.StepOver = 20
|
|
obj.LiftDistance=0
|
|
# obj.ProcessHoles = True
|
|
obj.ForceInsideOut = False
|
|
obj.Stopped = False
|
|
obj.StopProcessing = False
|
|
obj.HelixAngle = 5
|
|
obj.HelixDiameterLimit = 0.0
|
|
obj.AdaptiveInputState =""
|
|
obj.AdaptiveOutputState = ""
|
|
obj.StockToLeave= 0
|
|
obj.KeepToolDownRatio=3.0
|
|
|
|
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, obj = None):
|
|
'''Create(name) ... Creates and returns a Adaptive operation.'''
|
|
if obj is None:
|
|
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
|
|
proxy = PathAdaptive(obj,name)
|
|
return obj
|