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 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