diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index 37a60d27d3..82c0a41384 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -105,6 +105,8 @@ SET(PathScripts_SRCS PathScripts/PathUtils.py PathScripts/PathSimulatorGui.py PathScripts/PostUtils.py + PathScripts/PathAdaptiveGui.py + PathScripts/PathAdaptive.py PathScripts/__init__.py ) diff --git a/src/Mod/Path/Gui/Resources/Path.qrc b/src/Mod/Path/Gui/Resources/Path.qrc index a9889ef325..e0b7409d39 100644 --- a/src/Mod/Path/Gui/Resources/Path.qrc +++ b/src/Mod/Path/Gui/Resources/Path.qrc @@ -75,6 +75,7 @@ icons/edge-join-round.svg icons/edge-join-round-not.svg icons/preferences-path.svg + icons/Path-Adaptive.svg panels/DlgJobChooser.ui panels/DlgJobCreate.ui panels/DlgJobModelSelect.ui diff --git a/src/Mod/Path/Gui/Resources/icons/Path-Adaptive.svg b/src/Mod/Path/Gui/Resources/icons/Path-Adaptive.svg new file mode 100644 index 0000000000..f4ec64afa3 --- /dev/null +++ b/src/Mod/Path/Gui/Resources/icons/Path-Adaptive.svg @@ -0,0 +1,636 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + Path-FaceProfile + 2016-01-19 + http://www.freecadweb.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/Mod/Path/Gui/Resources/icons/Path- + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html + + + [agryson] Alexander Gryson + + + + + + + + + + A + + diff --git a/src/Mod/Path/InitGui.py b/src/Mod/Path/InitGui.py index dd6363b304..aad5d7ee52 100644 --- a/src/Mod/Path/InitGui.py +++ b/src/Mod/Path/InitGui.py @@ -102,13 +102,14 @@ class PathWorkbench (Workbench): from PathScripts import PathToolController from PathScripts import PathToolLibraryManager from PathScripts import PathSimulatorGui + from PathScripts import PathAdaptiveGui import PathCommands # build commands list projcmdlist = ["Path_Job", "Path_Post"] toolcmdlist = ["Path_Inspect", "Path_Simulator", "Path_ToolLibraryEdit", "Path_SelectLoop"] prepcmdlist = ["Path_Fixture", "Path_Comment", "Path_Stop", "Path_Custom"] - twodopcmdlist = ["Path_Contour", "Path_Profile_Faces", "Path_Profile_Edges", "Path_Pocket_Shape", "Path_Drilling", "Path_MillFace", "Path_Helix"] + twodopcmdlist = ["Path_Contour", "Path_Profile_Faces", "Path_Profile_Edges", "Path_Pocket_Shape", "Path_Drilling", "Path_MillFace", "Path_Helix", "Path_Adaptive"] threedopcmdlist = ["Path_Pocket_3D"] engravecmdlist = ["Path_Engrave"] modcmdlist = ["Path_OperationCopy", "Path_Array", "Path_SimpleCopy" ] diff --git a/src/Mod/Path/PathScripts/PathAdaptive.py b/src/Mod/Path/PathScripts/PathAdaptive.py new file mode 100644 index 0000000000..a2b92cf394 --- /dev/null +++ b/src/Mod/Path/PathScripts/PathAdaptive.py @@ -0,0 +1,421 @@ +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 = [] + #print "Input edges , remove duplicate projections to xy plane" + 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) + #print "remaining:", remaining + + newPath=True + while len(remaining)>0: + if newPath: + #print "new iteration" + edge=remaining[0] + p1 = edge.valueAt(edge.FirstParameter) + p2 = edge.valueAt(edge.LastParameter) + #print edge, p1, p2 + 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) + #print "chk",e, p1, p2 + if IsEqualInXYPlane(lastPoint,p1): + #print "last Point equal p1" + combined.append(discretize(e)) + remaining.remove(e) + lastPoint=p2 + anyMatch=True + break + elif IsEqualInXYPlane(lastPoint,p2): + #print "reversed" + 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: + #print (base,subs) + 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 diff --git a/src/Mod/Path/PathScripts/PathAdaptiveGui.py b/src/Mod/Path/PathScripts/PathAdaptiveGui.py new file mode 100644 index 0000000000..9c181b55be --- /dev/null +++ b/src/Mod/Path/PathScripts/PathAdaptiveGui.py @@ -0,0 +1,168 @@ +import FreeCAD +import FreeCADGui +import PathScripts.PathLog as PathLog +import PathScripts.PathGui as PathGui +import PathScripts.PathOpGui as PathOpGui +from PySide import QtCore, QtGui +import PathAdaptive + +class TaskPanelOpPage(PathOpGui.TaskPanelPage): + def initPage(self, obj): + self.setTitle("Adaptive path operation") + + def getForm(self): + form = QtGui.QWidget() + layout = QtGui.QVBoxLayout() + + #tool contoller + hlayout = QtGui.QHBoxLayout() + form.ToolController = QtGui.QComboBox() + form.ToolControllerLabel=QtGui.QLabel("Tool Controller") + hlayout.addWidget(form.ToolControllerLabel) + hlayout.addWidget(form.ToolController) + layout.addLayout(hlayout) + + #cut region + formLayout = QtGui.QFormLayout() + form.Side = QtGui.QComboBox() + form.Side.addItem("Inside") + form.Side.addItem("Outside") + form.Side.setToolTip("Cut inside or outside of the selected face") + formLayout.addRow(QtGui.QLabel("Cut Region"),form.Side) + + #operation type + form.OperationType = QtGui.QComboBox() + form.OperationType.addItem("Clearing") + form.OperationType.addItem("Profiling") + form.OperationType.setToolTip("Type of adaptive operation") + formLayout.addRow(QtGui.QLabel("Operation Type"),form.OperationType) + + #step over + form.StepOver = QtGui.QSpinBox() + form.StepOver.setMinimum(15) + form.StepOver.setMaximum(50) + form.StepOver.setSingleStep(1) + form.StepOver.setValue(25) + form.StepOver.setToolTip("Tool step over percentage") + formLayout.addRow(QtGui.QLabel("Step Over Percent"),form.StepOver) + + #tolerance + form.Tolerance = QtGui.QSlider(QtCore.Qt.Horizontal) + form.Tolerance.setMinimum(5) + form.Tolerance.setMaximum(15) + form.Tolerance.setTickInterval(1) + form.Tolerance.setValue(10) + form.Tolerance.setTickPosition(QtGui.QSlider.TicksBelow) + form.Tolerance.setToolTip("Influences calculation performace vs stability and accuracy") + formLayout.addRow(QtGui.QLabel("Accuracy vs Performance"),form.Tolerance) + + #helix angle + form.HelixAngle = QtGui.QDoubleSpinBox() + form.HelixAngle.setMinimum(0.1) + form.HelixAngle.setMaximum(90) + form.HelixAngle.setSingleStep(0.1) + form.HelixAngle.setValue(5) + form.HelixAngle.setToolTip("Angle of the helix ramp entry") + formLayout.addRow(QtGui.QLabel("Helix Ramp Angle"),form.HelixAngle) + + #helix diam. limit + form.HelixDiameterLimit = QtGui.QDoubleSpinBox() + form.HelixDiameterLimit.setMinimum(0.0) + form.HelixDiameterLimit.setMaximum(90) + form.HelixDiameterLimit.setSingleStep(0.1) + form.HelixDiameterLimit.setValue(0) + form.HelixDiameterLimit.setToolTip("If non zero it limits the size helix diameter, otherwise the tool radius is taken as the helix diameter") + formLayout.addRow(QtGui.QLabel("Helix Max Diameter"),form.HelixDiameterLimit) + + #lift distance + form.LiftDistance = QtGui.QDoubleSpinBox() + form.LiftDistance.setMinimum(0.0) + form.LiftDistance.setMaximum(1000) + form.LiftDistance.setSingleStep(0.1) + form.LiftDistance.setValue(1.0) + form.LiftDistance.setToolTip("How much to lift the tool up during the rapid repositioning moves (used when no obstacles)") + formLayout.addRow(QtGui.QLabel("Lift Distance"),form.LiftDistance) + + #process holes + form.ProcessHoles = QtGui.QCheckBox() + form.ProcessHoles.setChecked(True) + formLayout.addRow(QtGui.QLabel("Process Holes"),form.ProcessHoles) + + layout.addLayout(formLayout) + + #stop button + form.StopButton=QtGui.QPushButton("Stop") + form.StopButton.setCheckable(True) + layout.addWidget(form.StopButton) + + form.setLayout(layout) + return form + + def getSignalsForUpdate(self, obj): + '''getSignalsForUpdate(obj) ... return list of signals for updating obj''' + signals = [] + #signals.append(self.form.button.clicked) + signals.append(self.form.Side.currentIndexChanged) + signals.append(self.form.OperationType.currentIndexChanged) + signals.append(self.form.ToolController.currentIndexChanged) + signals.append(self.form.StepOver.valueChanged) + signals.append(self.form.Tolerance.valueChanged) + signals.append(self.form.HelixAngle.valueChanged) + signals.append(self.form.HelixDiameterLimit.valueChanged) + signals.append(self.form.LiftDistance.valueChanged) + + signals.append(self.form.ProcessHoles.stateChanged) + signals.append(self.form.StopButton.toggled) + return signals + + def setFields(self, obj): + self.selectInComboBox(obj.Side, self.form.Side) + self.selectInComboBox(obj.OperationType, self.form.OperationType) + self.form.StepOver.setValue(obj.StepOver) + self.form.Tolerance.setValue(int(obj.Tolerance*100)) + self.form.HelixAngle.setValue(obj.HelixAngle) + self.form.HelixDiameterLimit.setValue(obj.HelixDiameterLimit) + self.form.LiftDistance.setValue(obj.LiftDistance) + + self.form.ProcessHoles.setChecked(obj.ProcessHoles) + self.setupToolController(obj, self.form.ToolController) + self.form.StopButton.setChecked(obj.Stopped) + obj.setEditorMode('AdaptiveInputState', 2) #hide this property + obj.setEditorMode('AdaptiveOutputState', 2) #hide this property + obj.setEditorMode('StopProcessing', 2) # hide this property + obj.setEditorMode('Stopped', 2) # hide this property + + def getFields(self, obj): + if obj.Side != str(self.form.Side.currentText()): + obj.Side = str(self.form.Side.currentText()) + + if obj.OperationType != str(self.form.OperationType.currentText()): + obj.OperationType = str(self.form.OperationType.currentText()) + + obj.StepOver = self.form.StepOver.value() + obj.Tolerance = 1.0*self.form.Tolerance.value()/100.0 + obj.HelixAngle = self.form.HelixAngle.value() + obj.HelixDiameterLimit = self.form.HelixDiameterLimit.value() + obj.LiftDistance = self.form.LiftDistance.value() + + obj.ProcessHoles = self.form.ProcessHoles.isChecked() + obj.Stopped = self.form.StopButton.isChecked() + if(obj.Stopped): + self.form.StopButton.setChecked(False) #reset the button + obj.StopProcessing=True + + self.updateToolController(obj, self.form.ToolController) + obj.setEditorMode('AdaptiveInputState', 2) #hide this property + obj.setEditorMode('AdaptiveOutputState', 2) #hide this property + obj.setEditorMode('StopProcessing', 2) # hide this property + obj.setEditorMode('Stopped', 2) # hide this property + + + + +Command = PathOpGui.SetupOperation('Adaptive', + PathAdaptive.Create, + TaskPanelOpPage, + 'Path-Adaptive', + QtCore.QT_TRANSLATE_NOOP("PathAdaptive", "Adaptive"), + QtCore.QT_TRANSLATE_NOOP("PathPocket", "Adaptive clearing and profiling")) diff --git a/src/Mod/Path/PathScripts/PathSelection.py b/src/Mod/Path/PathScripts/PathSelection.py index 9b414cd2c6..0d1123b249 100644 --- a/src/Mod/Path/PathScripts/PathSelection.py +++ b/src/Mod/Path/PathScripts/PathSelection.py @@ -157,6 +157,31 @@ class POCKETGate: return pocketable +class ADAPTIVEGate: + def allow(self, doc, obj, sub): + + adaptive = False + try: + obj = obj.Shape + except: + return False + + if obj.ShapeType == 'Edge': + adaptive = False + + elif obj.ShapeType == 'Face': + adaptive = True + + elif obj.ShapeType == 'Solid': + if sub and sub[0:4] == 'Face': + adaptive = True + + elif obj.ShapeType == 'Compound': + if sub and sub[0:4] == 'Face': + adaptive = True + + return adaptive + class CONTOURGate: def allow(self, doc, obj, sub): pass @@ -189,6 +214,10 @@ def pocketselect(): FreeCADGui.Selection.addSelectionGate(POCKETGate()) FreeCAD.Console.PrintWarning("Pocketing Select Mode\n") +def adaptiveselect(): + FreeCADGui.Selection.addSelectionGate(ADAPTIVEGate()) + FreeCAD.Console.PrintWarning("Adaptive Select Mode\n") + def surfaceselect(): FreeCADGui.Selection.addSelectionGate(MESHGate()) FreeCAD.Console.PrintWarning("Surfacing Select Mode\n") @@ -207,6 +236,7 @@ def select(op): opsel['Profile Edges'] = eselect opsel['Profile Faces'] = profileselect opsel['Surface'] = surfaceselect + opsel['Adaptive'] = adaptiveselect return opsel[op] def clear(): diff --git a/src/Mod/Path/libarea/Adaptive.cpp b/src/Mod/Path/libarea/Adaptive.cpp new file mode 100644 index 0000000000..4d5b56f8d7 --- /dev/null +++ b/src/Mod/Path/libarea/Adaptive.cpp @@ -0,0 +1,1451 @@ +#include "Adaptive.hpp" +#include +#include +#include +#include + + +namespace ClipperLib { + void TranslatePath(const Path& input, Path& output, IntPoint delta); +} + +namespace AdaptivePath { + using namespace ClipperLib; + using namespace std; + + + inline double DistanceSqrd(const IntPoint& pt1, const IntPoint& pt2) + { + double Dx = ((double)pt1.X - pt2.X); + double dy = ((double)pt1.Y - pt2.Y); + return (Dx*Dx + dy*dy); + } + + inline bool SetSegmentLength(const IntPoint& pt1, IntPoint& pt2, double new_length) + { + double Dx = ((double)pt2.X - pt1.X); + double dy = ((double)pt2.Y - pt1.Y); + double l=sqrt(Dx*Dx + dy*dy); + if(l>0.0) { + pt2.X = pt1.X + new_length * Dx/l; + pt2.Y = pt1.Y + new_length * dy/l; + return true; + } + return false; + } + + /********************************************* + * Utils + ***********************************************/ + /* inline util*/ + inline bool HasAnyPath(const Paths &paths) { + for(Paths::size_type i=0;i0) return true; + } + return false; + } + + inline double averageDV(const vector & vec) { + double s=0; + std::size_t size = vec.size(); + if(size==0) return 0; + for(std::size_t i=0;i &unityVectors, DoublePoint& output) { + int size=unityVectors.size(); + output.X =0; + output.Y=0; + // sum vectors + for(int i=0;ilsegLenSqr) parameter=lsegLenSqr; + } + // point on line at parameter + closestPoint.X = p1.X + parameter*D21X/lsegLenSqr; + closestPoint.Y = p1.Y + parameter*D21Y/lsegLenSqr; + // calculate distance from point on line to pt + double DX=double(pt.X-closestPoint.X); + double DY=double(pt.Y-closestPoint.Y); + return DX*DX+DY*DY; // return distance squared + } + + // joins collinear segments (within the tolerance) + void CleanPath(const Path &inp, Path &outp, double tolerance) { + bool first=true; + outp.clear(); + for(const auto & pt : inp) { + if(first) { + first=false; + outp.push_back(pt); + } else { + if(outp.size()>2) { + IntPoint clp; // to hold closest point + double distSqrd = DistancePointToLineSegSquared(outp[outp.size()-2],outp[outp.size()-1],pt,clp,false); + if(sqrt(distSqrd)size(); + // iterate through segments + for(Path::size_type j=0;jat(j>0 ? j-1 : size-1),path->at(j),pt,clp); + if(distSq & intersections ) { + double DX = double(c2.X - c1.X); + double DY = double(c2.Y - c1.Y); + double d = sqrt(DX*DX+DY*DY); + if(d=radius) return false; // do not intersect, or intersect in one point (this case not relevant here) + double a_2 = sqrt(4*radius*radius-d*d)/2.0; + intersections.first = DoublePoint(0.5*(c1.X+c2.X)-DY*a_2/d, 0.5*(c1.Y+c2.Y)+DX*a_2/d); + intersections.second = DoublePoint(0.5*(c1.X+c2.X)+DY*a_2/d, 0.5*(c1.Y+c2.Y)-DX*a_2/d); + return true; + } + + inline double PointSideOfLine(const IntPoint& p1, const IntPoint& p2,const IntPoint& pt) { + return (pt.X - p1.X)*(p2.Y-p1.Y) - (pt.Y - p2.Y)*(p2.X-p1.X); + } + + inline double Angle3Points(const DoublePoint & p1,const DoublePoint& p2, const DoublePoint& p3) { + double t1= atan2(p1.Y-p2.Y,p1.X-p2.X); + double t2=atan2(p3.Y-p2.Y,p3.X-p2.X); + double a = fabs( t2 - t1 ); + return min(a,2*M_PI-a); + } + + bool Line2CircleIntersect(const IntPoint &c, double radius,const IntPoint &p1, const IntPoint &p2, vector & result, bool clamp=true) + { + // if more intersections returned, first is closer to p1 + //to do: box check for performance + double dx=double(p2.X-p1.X); + double dy=double(p2.Y-p1.Y); + double lcx = double(p1.X - c.X); + double lcy = double(p1.Y - c.Y); + double a=dx*dx+dy*dy; + double b=2*dx*lcx+2*dy*lcy; + double C=lcx*lcx+lcy*lcy-radius*radius; + double sq = b*b-4*a*C; + if (sq<0) return false; // no solution + sq=sqrt(sq); + double t1=(-b-sq)/(2*a); + double t2=(-b+sq)/(2*a); + result.clear(); + if(clamp) { + if (t1>=0.0 && t1<=1.0) result.push_back(DoublePoint(p1.X + t1*dx, p1.Y + t1*dy)); + if (t2>=0.0 && t2<=1.0) result.push_back(DoublePoint(p1.X + t2*dx, p1.Y + t2*dy)); + } else { + result.push_back(DoublePoint(p1.X + t2*dx, p1.Y + t2*dy)); + result.push_back(DoublePoint(p1.X + t2*dx, p1.Y + t2*dy)); + } + return result.size()>0; + } + + // calculate center point of polygon + IntPoint Compute2DPolygonCentroid(const Path &vertices) + { + IntPoint centroid(0,0); + double signedArea = 0.0; + double x0 = 0.0; // Current vertex X + double y0 = 0.0; // Current vertex Y + double x1 = 0.0; // Next vertex X + double y1 = 0.0; // Next vertex Y + double a = 0.0; // Partial signed area + + // For all vertices + size_t i=0; + Path::size_type size = vertices.size(); + for (i=0; i0 && pip!=0) return false; // is inside hole + } + return true; + } + + /* finds intersection of line segment with line segment */ + bool IntersectionPoint(const IntPoint & s1p1, + const IntPoint & s1p2, + const IntPoint & s2p1, + const IntPoint & s2p2, + IntPoint & intersection) { + // todo: bounds check for perfomance + double S1DX = double(s1p2.X - s1p1.X); + double S1DY = double(s1p2.Y - s1p1.Y); + double S2DX = double(s2p2.X - s2p1.X); + double S2DY = double(s2p2.Y - s2p1.Y); + double d=S1DY*S2DX - S2DY*S1DX; + if(fabs(d)0 || p2d0 + )) return false ; // intersection not within segment1 + if((d>0) && ( + p1d<0 || p1d>d || p2d<0 || p2d>d + )) return true; // intersection not within segment2 + double t=p1d/d; + intersection=IntPoint(s1p1.X + S1DX*t, s1p1.Y + S1DY*t); + return true; + } + + /* finds one/first intersection of line segment with paths */ + bool IntersectionPoint(const Paths & paths,const IntPoint & p1, const IntPoint & p2, IntPoint & intersection) { + for(size_t i=0; i< paths.size(); i++) { + const Path *path = &paths[i]; + size_t size=path->size(); + if(size<2) continue; + for(size_t j=0;jat(j>0?j-1:size-1); + const IntPoint * pp2 = &path->at(j); + double LDY = double(p2.Y - p1.Y); + double LDX = double(p2.X - p1.X); + double PDX = double(pp2->X - pp1->X); + double PDY = double(pp2->Y - pp1->Y); + double d=LDY*PDX - PDY*LDX; + if(fabs(d)X); + double LPDY = double(p1.Y - pp1->Y); + double p1d = PDY*LPDX - PDX*LPDY; + double p2d = LDY*LPDX - LDX*LPDY; + if((d<0) && ( + p1d0 || p2d0 + )) continue; // intersection not within segment + if((d>0) && ( + p1d<0 || p1d>d || p2d<0 || p2d>d + )) continue; // intersection not within segment + double t=p1d/d; + intersection=IntPoint(p1.X + LDX*t, p1.Y + LDY*t); + return true; + } + } + return false; + } + + // helper class for measuring performance + class PerfCounter { + public: + PerfCounter(string p_name) { + name = p_name; + count =0; + } + void Start() { + start_ticks=clock(); + } + void Stop() { + total_ticks+=clock()-start_ticks; + count++; + } + void DumpResults() { + double total_time=double(total_ticks)/CLOCKS_PER_SEC; + cout<<"Perf: " << name.c_str() << " total_time: " << total_time << " sec, call_count:" << count << " per_call:" << double(total_time/count) << endl; + } + private: + string name; + clock_t start_ticks; + clock_t total_ticks; + size_t count; + }; + + PerfCounter Perf_ProcessPolyNode("ProcessPolyNode"); + PerfCounter Perf_CalcCutArea("CalcCutArea"); + PerfCounter Perf_NextEngagePoint("NextEngagePoint"); + PerfCounter Perf_PointIterations("PointIterations"); + PerfCounter Perf_ExpandCleared("ExpandCleared"); + PerfCounter Perf_DistanceToBoundary("DistanceToBoundary"); + + /***************************************** + * Linear Interpolation - area vs angle + * ***************************************/ + class Interpolation { + public: + const double MIN_ANGLE = -M_PI/4; + const double MAX_ANGLE = M_PI/4; + + void clear() { + angles.clear(); + areas.clear(); + } + // adds point keeping the incremental order of areas in order for interpolation to work correctly + void addPoint(double area, double angle) { + std::size_t size = areas.size(); + if(size==0 || area > areas[size-1] + NTOL) { // first point or largest area point + areas.push_back(area); + angles.push_back(angle); + return; + } + + for(std::size_t i=0;i areas[i-1] + NTOL)) { + areas.insert(areas.begin() + i,area); + angles.insert(angles.begin() + i,angle); + } + } + } + + double interpolateAngle(double targetArea) { + std::size_t size = areas.size(); + if(size<2 || targetArea>areas[size-1]) return MIN_ANGLE; //max engage angle - convinient value to initially measure cut area + if(targetAreatargetArea) { + // linear interpolation + double af = (targetArea-areas[i-1])/(areas[i] - areas[i-1]); + double a = angles[i-1] + af*(angles[i] - angles[i-1]); + return a; + } + } + return MIN_ANGLE; + } + + double clampAngle(double angle) { + if(angleMAX_ANGLE) return MAX_ANGLE; + return angle; + } + + double getRandomAngle() { + return MIN_ANGLE + (MAX_ANGLE-MIN_ANGLE)*double(rand())/double(RAND_MAX); + } + size_t getPointCount() { + return areas.size(); + } + + private: + vector angles; + vector areas; + + }; + + /**************************************** + * Engage Point + ***************************************/ + class EngagePoint { + public: + EngagePoint(const Paths & p_toolBoundPaths) { + toolBoundPaths=&p_toolBoundPaths; + currentPathIndex=0; + currentSegmentIndex=0; + segmentPos =0; + totalDistance=0; + calculateCurrentPathLength(); + } + + + void moveToClosestPoint(const IntPoint &pt,double step) { + double minDistSq = __DBL_MAX__; + size_t minPathIndex = currentPathIndex; + size_t minSegmentIndex = currentSegmentIndex; + double minSegmentPos = segmentPos; + totalDistance=0; + for(;;) { + while(moveForward(step)) { + double distSqrd = DistanceSqrd(pt,getCurrentPoint()); + if(distSqrd1) { + Perf_NextEngagePoint.Stop(); + return false; // nothin more to cut + } + prevArea=0; + } + } + IntPoint cpt = getCurrentPoint(); + double area=parent->CalcCutArea(clip,initialPoint,cpt,cleared); + //cout << "engage scan path: " << currentPathIndex << " distance:" << totalDistance << " area:" << area << " areaPD:" << area/step << " min:" << minCutArea << " max:" << maxCutArea << endl; + if(area>minCutArea && areaprevArea) { + Perf_NextEngagePoint.Stop(); + return true; + } + prevArea=area; + } + } + IntPoint getCurrentPoint() { + const Path * pth = &toolBoundPaths->at(currentPathIndex); + const IntPoint * p1=&pth->at(currentSegmentIndex>0?currentSegmentIndex-1:pth->size()-1); + const IntPoint * p2=&pth->at(currentSegmentIndex); + double segLength =sqrt(DistanceSqrd(*p1,*p2)); + return IntPoint(p1->X + segmentPos*double(p2->X-p1->X)/segLength,p1->Y + segmentPos*double(p2->Y-p1->Y)/segLength); + } + + DoublePoint getCurrentDir() { + const Path * pth = &toolBoundPaths->at(currentPathIndex); + const IntPoint * p1=&pth->at(currentSegmentIndex>0?currentSegmentIndex-1:pth->size()-1); + const IntPoint * p2=&pth->at(currentSegmentIndex); + double segLength =sqrt(DistanceSqrd(*p1,*p2)); + return DoublePoint(double(p2->X-p1->X)/segLength,double(p2->Y-p1->Y)/segLength); + } + + bool moveForward(double distance) { + const Path * pth = &toolBoundPaths->at(currentPathIndex); + if(distancesegmentLength) { + currentSegmentIndex++; + if(currentSegmentIndex>=pth->size()) { + currentSegmentIndex=0; + } + distance=distance-(segmentLength-segmentPos); + segmentPos =0; + segmentLength =currentSegmentLength(); + } + segmentPos+=distance; + return totalDistance<=1.2 * currentPathLength; + } + + bool nextPath() { + currentPathIndex++; + currentSegmentIndex=0; + segmentPos =0; + totalDistance=0; + if(currentPathIndex>=toolBoundPaths->size()) { + currentPathIndex =0; + calculateCurrentPathLength(); + return false; + } + calculateCurrentPathLength(); + //cout << "nextPath:" << currentPathIndex << endl; + return true; + } + + private: + const Paths * toolBoundPaths; + size_t currentPathIndex; + size_t currentSegmentIndex; + double segmentPos =0; + double totalDistance=0; + double currentPathLength=0; + int passes=0; + Clipper clip; + void calculateCurrentPathLength() { + const Path * pth = &toolBoundPaths->at(currentPathIndex); + size_t size=pth->size(); + currentPathLength=0; + for(size_t i=0;iat(i>0?i-1:size-1); + const IntPoint * p2=&pth->at(i); + currentPathLength += sqrt(DistanceSqrd(*p1,*p2)); + } + } + + double currentSegmentLength() { + const Path * pth = &toolBoundPaths->at(currentPathIndex); + const IntPoint * p1=&pth->at(currentSegmentIndex>0?currentSegmentIndex-1:pth->size()-1); + const IntPoint * p2=&pth->at(currentSegmentIndex); + return sqrt(DistanceSqrd(*p1,*p2)); + } + + + + + + + }; + /**************************************** + // Adaptive2d - constructor + *****************************************/ + Adaptive2d::Adaptive2d() { + } + + double Adaptive2d::CalcCutArea(Clipper & clip,const IntPoint &c1, const IntPoint &c2, const Paths &cleared_paths) { + Perf_CalcCutArea.Start(); + + double dist = DistanceSqrd(c1,c2); + if(dist inters; // to hold intersection results + + for(const Path &path : cleared_paths) { + size_t size = path.size(); + size_t curPtIndex = 0; + bool found=false; + // step 1: we find the starting point on the cleared path that is outside new tool shape (c2) + for(size_t i=0;irsqrd) { + found = true; + break; + } + curPtIndex++; if(curPtIndex>=size) curPtIndex=0; + } + if(!found) continue; // try anohter path + + // step 2: iterate throuh path from starting point and find the part of the path inside the c2 + size_t prevPtIndex = curPtIndex; + Path *interPath; + bool prev_inside=false; + const IntPoint *p1=&path[prevPtIndex]; + + for(size_t i=0;i=size) curPtIndex=0; + const IntPoint *p2=&path[curPtIndex]; + if(!prev_inside) { // prev state: outside, find first point inside C2 + // TODO:BBOX check here maybe + if(DistancePointToLineSegSquared(*p1,*p2,c2, clp)<=rsqrd) { // current segment inside, start + prev_inside=true; + interPaths.push_back(Path()); + if(interPaths.size()>1) break; // we will use poly clipping alg. if there are more intersecting paths + interPath=&interPaths.back(); + // current segment inside c2, prev point outside, find intersection: + if(Line2CircleIntersect(c2,toolRadiusScaled,*p1,*p2,inters)) { + interPath->push_back(IntPoint(inters[0].X,inters[0].Y)); + if(inters.size()>1) { + interPath->push_back(IntPoint(inters[1].X,inters[1].Y)); + prev_inside=false; + } else { + interPath->push_back(IntPoint(*p2)); + } + } else { // no intersection - must be edge case, add p2 + //prev_inside=false; + interPath->push_back(IntPoint(*p2)); + } + } + } else { // state: inside + if( (DistanceSqrd(c2,*p2) <= rsqrd)) { // next point still inside, add it and continue, no state change + interPath->push_back(IntPoint(*p2)); + } else { // prev point inside, current point outside, find instersection + if(Line2CircleIntersect(c2,toolRadiusScaled,*p1,*p2,inters)) { + if(inters.size()>1) { + interPath->push_back(IntPoint(inters[1].X,inters[1].Y)); + } else { + interPath->push_back(IntPoint(inters[0].X,inters[0].Y)); + } + } + prev_inside=false; + } + } + prevPtIndex = curPtIndex; + p1 = p2; + + } + if(interPaths.size()>1) break; // we will use poly clipping alg. if there are more intersecting paths with the tool (rare case) + } + + if(interPaths.size()==1 && interPaths.front().size()>1 ) { + Path *interPath=&interPaths.front(); + // interPath - now contains the part of cleared path inside the C2 + size_t ipc2_size =interPath->size(); + const IntPoint &fpc2=interPath->front(); // first point + const IntPoint &lpc2=interPath->back(); // last point + // path length + double interPathLen=0; + for(size_t j=1;jat(j-1),interPath->at(j))); + + Paths inPaths; + inPaths.reserve(200); + inPaths.push_back(*interPath); + Path pthToSubtract ; + pthToSubtract.push_back(fpc2); + + double fi1 = atan2(fpc2.Y-c2.Y,fpc2.X-c2.X); + double fi2 = atan2(lpc2.Y-c2.Y,lpc2.X-c2.X); + double minFi=fi1; + double maxFi=fi2; + if(maxFi=RESOLUTION_FACTOR && !IsPointWithinCutRegion(cleared_paths,c2)) { + if(PointSideOfLine(fpc2,lpc2,c2)<0) { + IntPoint midPoint(c2.X + toolRadiusScaled*cos(0.5*(maxFi+minFi)),c2.Y + toolRadiusScaled*sin(0.5*(maxFi+minFi))); + if(PointSideOfLine(fpc2,lpc2,midPoint)>0) { + area = __DBL_MAX__; + Perf_CalcCutArea.Stop(); + #ifdef DEV_MODE + cout << "Break: @(" << double(c2.X)/scaleFactor << "," << double(c2.Y)/scaleFactor << ") conventional mode" << endl; + #endif + return area; + } + } + } + } + + double scanDistance = 2.5*toolRadiusScaled; + // stepping through path discretized to stepDistance + double stepDistance=min(double(RESOLUTION_FACTOR),interPathLen/24)+1; + //cout << stepDistance << endl; + const IntPoint * prevPt=&interPath->front(); + double distance=0; + for(size_t j=1;jat(j); + double segLen = sqrt(DistanceSqrd(*cpt,*prevPt)); + if(segLensegLen) { + distance+=stepDistance-(pos-segLen); + pos=segLen; // make sure we get exact end point + } else { + distance+=stepDistance; + } + double dx=double(cpt->X-prevPt->X); + double dy=double(cpt->Y-prevPt->Y); + IntPoint segPoint(prevPt->X + dx*pos/segLen, prevPt->Y + dy*pos/segLen); + IntPoint scanPoint(c2.X + scanDistance*cos(minFi + distance*(maxFi-minFi)/interPathLen), + c2.Y + scanDistance*sin(minFi + distance*(maxFi-minFi)/interPathLen)); + + IntPoint intersC2(segPoint.X,segPoint.Y); + IntPoint intersC1(segPoint.X,segPoint.Y); + + // there should be intersection with C2 + if(Line2CircleIntersect(c2,toolRadiusScaled,segPoint,scanPoint,inters)) { + if(inters.size()>1) { + intersC2.X = inters[1].X; + intersC2.Y = inters[1].Y; + } else { + intersC2.X = inters[0].X; + intersC2.Y = inters[0].Y; + } + } else { + pthToSubtract.push_back(segPoint); + } + + if(Line2CircleIntersect(c1,toolRadiusScaled,segPoint,scanPoint,inters)) { + if(inters.size()>1) { + intersC1.X = inters[1].X; + intersC1.Y = inters[1].Y; + } else { + intersC1.X = inters[0].X; + intersC1.Y = inters[0].Y; + } + if(DistanceSqrd(segPoint,intersC2)1) + { + // old way of calculating cut area based on polygon slipping + // used in case when there are multiple intersections of tool with cleared poly (very rare case, but important) + // 1. find differene beween old and new tool shape + Path oldTool; + Path newTool; + TranslatePath(toolGeometry,oldTool,c1); + TranslatePath(toolGeometry,newTool,c2); + clip.Clear(); + clip.AddPath(newTool, PolyType::ptSubject, true); + clip.AddPath(oldTool, PolyType::ptClip, true); + Paths toolDiff; + clip.Execute(ClipType::ctDifference,toolDiff); + + // 2. difference to cleared + clip.Clear(); + clip.AddPaths(toolDiff,PolyType::ptSubject, true); + clip.AddPaths(cleared_paths,PolyType::ptClip, true); + Paths cutAreaPoly; + clip.Execute(ClipType::ctDifference, cutAreaPoly); + + // calculate resulting area + area=0; + for(Path &path : cutAreaPoly) { + area +=fabs(Area(path)); + } + } + // cout<< "PolyArea:" << areaSum << " new area:" << area << endl; + Perf_CalcCutArea.Stop(); + return area; + } + + /**************************************** + // Adaptive2d - Execute + *****************************************/ + std::list Adaptive2d::Execute(const DPaths &paths, std::function progressCallbackFn) { + //********************************** + // Initializations + // ********************************** + + // a keep the tolerance in workable range + if(tolerance<0.01) tolerance=0.01; + if(tolerance>0.2) tolerance=0.2; + + scaleFactor = RESOLUTION_FACTOR/tolerance; + toolRadiusScaled = toolDiameter*scaleFactor/2; + bbox_size =toolDiameter*scaleFactor; + progressCallback = &progressCallbackFn; + lastProgressTime=clock(); + stopProcessing=false; + + if(helixRampDiameter>toolDiameter) helixRampDiameter = toolDiameter; + if(helixRampDiameter pt = paths[i][j]; + cpth.push_back(IntPoint(pt.first*scaleFactor,pt.second*scaleFactor)); + } + inputPaths.push_back(cpth); + } + SimplifyPolygons(inputPaths); + + // ******************************* + // Resolve hierarchy and run processing + // ******************************** + + if(opType==OperationType::otClearing) { + clipof.Clear(); + clipof.AddPaths(inputPaths,JoinType::jtRound,EndType::etClosedPolygon); + PolyTree initialTree; + clipof.Execute(initialTree,-toolRadiusScaled-finishPassOffsetScaled); + PolyNode *current = initialTree.GetFirst(); + while(current!=0) { + int nesting = 0; + PolyNode *parent = current->Parent; + while(parent->Parent) { + nesting++; + parent=parent->Parent; + } + //cout<< " nesting:" << nesting << " limit:" << polyTreeNestingLimit << endl; + if(nesting%2!=0 && (polyTreeNestingLimit==0 || nesting<=polyTreeNestingLimit)) { + + Paths toolBoundPaths; + toolBoundPaths.push_back(current->Contour); + if(polyTreeNestingLimit != nesting) { + for(size_t i=0;i<(size_t)current->ChildCount();i++) + toolBoundPaths.push_back(current->Childs[i]->Contour); + } + + // calc bounding paths - i.e. area that must be cleared inside + // it's not the same as input paths due to filtering (nesting logic) + Paths boundPaths; + clipof.Clear(); + clipof.AddPaths(toolBoundPaths,JoinType::jtRound,EndType::etClosedPolygon); + clipof.Execute(boundPaths,toolRadiusScaled+finishPassOffsetScaled); + ProcessPolyNode(boundPaths,toolBoundPaths); + } + current = current->GetNext(); + } + } + + if(opType==OperationType::otProfilingInside || opType==OperationType::otProfilingOutside) { + double offset = opType==OperationType::otProfilingInside ? -2*(helixRampRadiusScaled+toolRadiusScaled)-RESOLUTION_FACTOR : 2*(helixRampRadiusScaled+toolRadiusScaled) + RESOLUTION_FACTOR; + clipof.Clear(); + clipof.AddPaths(inputPaths,JoinType::jtRound,EndType::etClosedPolygon); + PolyTree initialTree; + clipof.Execute(initialTree,0); + + PolyNode *current = initialTree.GetFirst(); + while(current!=0) { + int nesting = 0; + PolyNode *parent = current->Parent; + while(parent->Parent) { + nesting++; + parent=parent->Parent; + } + //cout<< " nesting:" << nesting << " limit:" << polyTreeNestingLimit << " processHoles:" << processHoles << endl; + if(nesting%2!=0 && (polyTreeNestingLimit==0 || nesting<=polyTreeNestingLimit)) { + Paths profilePaths; + profilePaths.push_back(current->Contour); + if(polyTreeNestingLimit != nesting) { + for(size_t i=0;i<(size_t)current->ChildCount();i++) profilePaths.push_back(current->Childs[i]->Contour); + } + for(size_t i=0;iGetNext(); + } + } + //cout<<" Adaptive2d::Execute finish" << endl; + return results; + } + + bool Adaptive2d::FindEntryPoint(const Paths & toolBoundPaths,const Paths &boundPaths, Paths &cleared /*output-initial cleard area by helix*/, IntPoint &entryPoint /*output*/) { + Paths incOffset; + Paths lastValidOffset; + Clipper clip; + ClipperOffset clipof; + bool found= false; + + Paths checkPaths = toolBoundPaths; + for(int iter=0;iter<10;iter++) { + clipof.Clear(); + clipof.AddPaths(checkPaths,JoinType::jtSquare,EndType::etClosedPolygon); + double step = RESOLUTION_FACTOR; + double currentDelta=-1; + clipof.Execute(incOffset,currentDelta); + while(incOffset.size()>0) { + clipof.Execute(incOffset,currentDelta); + if(incOffset.size()>0) lastValidOffset=incOffset; + currentDelta-=step; + } + for(size_t i=0;i0) { + entryPoint = Compute2DPolygonCentroid(lastValidOffset[i]); + //DrawPath(lastValidOffset[i],22); + found=true; + break; + } + } + // check if the start point is in any of the holes + // this may happen in case when toolBoundPaths are simetric (boundary + holes) + // we need to break simetry and try again + for(size_t j=0;j0 && pip!=0) ) { + found=false; + break; + } + } + // check if helix fits + if(found) { + // make initial polygon cleard by helix ramp + clipof.Clear(); + Path p1; + p1.push_back(entryPoint); + clipof.AddPath(p1,JoinType::jtRound,EndType::etOpenRound); + clipof.Execute(cleared,helixRampRadiusScaled+toolRadiusScaled); + CleanPolygons(cleared); + // we got first cleared area - check if it is crossing boundary + clip.Clear(); + clip.AddPaths(cleared,PolyType::ptSubject,true); + clip.AddPaths(boundPaths,PolyType::ptClip,true); + Paths crossing; + clip.Execute(ClipType::ctDifference,crossing); + if(crossing.size()>0) { + //cerr<<"Helix does not fit to the cutting area"<0 && output.AdaptivePaths.back().second.size()>0) { // if there is a previous path + auto & lastTPath = output.AdaptivePaths.back(); + auto & lastTPoint = lastTPath.second.back(); + IntPoint lastPoint(lastTPoint.first*scaleFactor,lastTPoint.second*scaleFactor); + bool clear = CheckCollision(lastPoint,nextPoint,cleared); + // add linking move + TPath linkPath; + linkPath.first = clear ? MotionType::mtLinkClear : MotionType::mtLinkNotClear; + DPoint nextT; + nextT.first = double(nextPoint.X)/scaleFactor; + nextT.second = double(nextPoint.Y)/scaleFactor; + linkPath.second.push_back(lastTPoint); + linkPath.second.push_back(nextT); + output.AdaptivePaths.push_back(linkPath); + // first we find the last point + } + TPath cutPath; + cutPath.first =MotionType::mtCutting; + for(const auto &p : passToolPath) { + DPoint nextT; + nextT.first = double(p.X)/scaleFactor; + nextT.second = double(p.Y)/scaleFactor; + cutPath.second.push_back(nextT); + } + + if(close) { + DPoint nextT; + nextT.first = double(passToolPath[0].X)/scaleFactor; + nextT.second = double(passToolPath[0].Y)/scaleFactor; + cutPath.second.push_back(nextT); + } + if(cutPath.second.size()>0) output.AdaptivePaths.push_back(cutPath); + } + + void Adaptive2d::CheckReportProgress(TPaths &progressPaths, bool force) { + if(!force && (clock()-lastProgressTimesecond.back(); + DPoint next(lastPoint->first,lastPoint->second); + while(progressPaths.size()>1) progressPaths.pop_back(); + while(progressPaths.front().second.size()>0) progressPaths.front().second.pop_back(); + progressPaths.front().second.push_back(next); + } + void Adaptive2d::ProcessPolyNode(Paths & boundPaths, Paths & toolBoundPaths) { + //cout << " Adaptive2d::ProcessPolyNode" << endl; + Perf_ProcessPolyNode.Start(); + // node paths are already constrained to tool boundary path for adaptive path before finishing pass + + IntPoint entryPoint; + TPaths progressPaths; + progressPaths.reserve(10000); + + SimplifyPolygons(toolBoundPaths); + CleanPolygons(toolBoundPaths); + SimplifyPolygons(boundPaths); + CleanPolygons(toolBoundPaths); + + Paths cleared; + if(!FindEntryPoint(toolBoundPaths, boundPaths, cleared, entryPoint)) return; + //cout << "Entry point:" << entryPoint << endl; + Clipper clip; + ClipperOffset clipof; + AdaptiveOutput output; + output.HelixCenterPoint.first = double(entryPoint.X)/scaleFactor; + output.HelixCenterPoint.second =double(entryPoint.Y)/scaleFactor; + + long stepScaled = RESOLUTION_FACTOR; + IntPoint engagePoint; + + IntPoint toolPos; + DoublePoint toolDir; + + IntPoint newToolPos; + DoublePoint newToolDir; + + // visualize/progress for helix + clipof.Clear(); + Path hp; + hp << entryPoint; + clipof.AddPath(hp,JoinType::jtRound,EndType::etOpenRound); + Paths hps; + clipof.Execute(hps,helixRampRadiusScaled); + progressPaths.push_back(TPath()); + + // show in progress cb + for(auto & pt:hps[0]) { + progressPaths.back().second.push_back(DPoint(double(pt.X)/scaleFactor,double(pt.Y)/scaleFactor)); + } + + // find the first tool position and direction + toolPos = IntPoint(entryPoint.X,entryPoint.Y - helixRampRadiusScaled); + toolDir = DoublePoint(1.0,0.0); + output.StartPoint =DPoint(double(toolPos.X)/scaleFactor,double(toolPos.Y)/scaleFactor); + + bool firstEngagePoint=true; + Path passToolPath; // to store pass toolpath + Path toClearPath; // to clear toolpath + IntPoint clp; // to store closest point + vector gyro; // used to average tool direction + vector angleHistory; // use to predict deflection angle + double angle = M_PI; + engagePoint = toolPos; + Interpolation interp; // interpolation instance + EngagePoint engage(toolBoundPaths); // engage point stepping instance + + long total_iterations =0; + long total_points =0; + long total_exceeded=0; + long total_output_points=0; + long over_cut_count =0; + //long engage_no_cut_count=0; + + double perf_total_len=0; + #ifdef DEV_MODE + clock_t start_clock=clock(); + #endif + + /******************************* + * LOOP - PASSES + *******************************/ + for(long pass=0;pass0) + progressPaths.push_back(TPath()); + } + + angle = M_PI_4; // initial pass angle + bool reachedBoundary = false; + double cumulativeCutArea=0; + // init gyro + gyro.clear(); + for(int i=0;i1e-5) { + stepScaled = RESOLUTION_FACTOR/fabs(angle); + } else { + stepScaled = RESOLUTION_FACTOR*4 ; + } + + + + // clamp the step size - for stability + + if(stepScaled>min(double(toolRadiusScaled/4), double(RESOLUTION_FACTOR*8))) + stepScaled=min(double(toolRadiusScaled/4), double(RESOLUTION_FACTOR*8)); + if(stepScaled ANGLE_HISTORY_POINTS) + angleHistory.erase(angleHistory.begin()); + break; + } + if(iteration>5 && fabs(error-prev_error)<0.001) break; + if(iteration==MAX_ITERATIONS-1) total_exceeded++; + prev_error = error; + } + Perf_PointIterations.Stop(); + /************************************************ + * CHECK AND RECORD NEW TOOL POS + * **********************************************/ + if(!IsPointWithinCutRegion(toolBoundPaths,newToolPos)) { + reachedBoundary=true; + // we reached end of cutting area + IntPoint boundaryPoint; + if(IntersectionPoint(toolBoundPaths,toolPos,newToolPos, boundaryPoint)) { + newToolPos=boundaryPoint; + area = CalcCutArea(clip,toolPos,newToolPos,cleared); + double dist = sqrt(DistanceSqrd(toolPos, newToolPos)); + if(dist>NTOL) + areaPD = area/double(dist); // area per distance + else { + areaPD=0; + area=0; + } + + } else { + newToolPos=toolPos; + area=0; + areaPD=0; + } + } + + if(area>stepScaled*optimalCutAreaPD && areaPD>2*optimalCutAreaPD) { // safety condition + over_cut_count++; + #ifdef DEV_MODE + cout<<"Break: over cut @" << point_index << "(" << double(toolPos.X)/scaleFactor << ","<< double(toolPos.Y)/scaleFactor << ")" + << " iter:" << iteration << " @bound:" << reachedBoundary << endl; + #endif + // ClearScreenFn(); + // DrawCircle(toolPos,toolRadiusScaled,0); + // DrawCircle(newToolPos,toolRadiusScaled,1); + // DrawPaths(cleared,22); + break; + } + + + if(firstEngagePoint) { // initial spiral shape need clearing in smaller intervals + double distFromEntry = sqrt(DistanceSqrd(toolPos,entryPoint)); + double circ = distFromEntry * M_PI; + //cout << (circ/(16*RESOLUTION_FACTOR)) << endl; + if(toClearPath.size()>circ/(16*RESOLUTION_FACTOR)) { + Perf_ExpandCleared.Start(); + // expand cleared + clipof.Clear(); + clipof.AddPath(toClearPath,JoinType::jtRound,EndType::etOpenRound); + Paths toolCoverPoly; + clipof.Execute(toolCoverPoly,toolRadiusScaled+1); + clip.Clear(); + clip.AddPaths(cleared,PolyType::ptSubject,true); + clip.AddPaths(toolCoverPoly,PolyType::ptClip,true); + clip.Execute(ClipType::ctUnion,cleared); + CleanPolygons(cleared); + toClearPath.clear(); + Perf_ExpandCleared.Stop(); + } + } + + if(area>0) { // cut is ok - record it + if(toClearPath.size()==0) toClearPath.push_back(toolPos); + toClearPath.push_back(newToolPos); + + cumulativeCutArea+=area; + + // append to toolpaths + if(passToolPath.size()==0) passToolPath.push_back(toolPos); + passToolPath.push_back(newToolPos); + perf_total_len+=stepScaled; + toolPos=newToolPos; + + // append to progress info paths + if(progressPaths.size()==0) { + progressPaths.push_back(TPath()); + } + progressPaths.back().second.push_back(DPoint(double(newToolPos.X)/scaleFactor,double(newToolPos.Y)/scaleFactor)); + + // apend gyro + gyro.push_back(newToolDir); + gyro.erase(gyro.begin()); + CheckReportProgress(progressPaths); + } else { + #ifdef DEV_MODE + // if(point_index==0) { + // engage_no_cut_count++; + // cout<<"Break:no cut #" << engage_no_cut_count << ", bad engage, pass:" << pass << " over_cut_count:" << over_cut_count << endl; + // } + #endif + //cerr<<"Break: no cut @" << point_index << endl; + break; + } + if(reachedBoundary) + break; + } /* end of points loop*/ + + if(toClearPath.size()>0) { + // expand cleared + Perf_ExpandCleared.Start(); + clipof.Clear(); + clipof.AddPath(toClearPath,JoinType::jtRound,EndType::etOpenRound); + Paths toolCoverPoly; + clipof.Execute(toolCoverPoly,toolRadiusScaled+1); + clip.Clear(); + clip.AddPaths(cleared,PolyType::ptSubject,true); + clip.AddPaths(toolCoverPoly,PolyType::ptClip,true); + clip.Execute(ClipType::ctUnion,cleared); + CleanPolygons(cleared); + toClearPath.clear(); + Perf_ExpandCleared.Stop(); + } + if(cumulativeCutArea>MIN_CUT_AREA_FACTOR*stepScaled*stepOverFactor*referenceCutArea) { + Path cleaned; + CleanPath(passToolPath,cleaned,CLEAN_PATH_TOLERANCE); + total_output_points+=cleaned.size(); + AppendToolPath(output,cleaned,cleared); + CheckReportProgress(progressPaths); + } + /*****NEXT ENGAGE POINT******/ + if(firstEngagePoint) { + engage.moveToClosestPoint(newToolPos,stepScaled+1); + firstEngagePoint=false; + } else { + double moveDistance = ENGAGE_SCAN_DISTANCE_FACTOR * RESOLUTION_FACTOR * 8; + if(!engage.nextEngagePoint(this, cleared,moveDistance,ENGAGE_AREA_THR_FACTOR*optimalCutAreaPD*RESOLUTION_FACTOR,4*referenceCutArea*stepOverFactor)) break; + } + toolPos = engage.getCurrentPoint(); + toolDir = engage.getCurrentDir(); + } + /**********************************/ + /* FINISHING PASS */ + /**********************************/ + clipof.Clear(); + clipof.AddPaths(boundPaths,JoinType::jtRound,EndType::etClosedPolygon); + Paths finishingPaths; + clipof.Execute(finishingPaths,-toolRadiusScaled); + IntPoint lastPoint; + + for(auto & pth: finishingPaths) { + progressPaths.push_back(TPath()); + // show in progress cb + for(auto & pt:pth) { + progressPaths.back().second.push_back(DPoint(double(pt.X)/scaleFactor,double(pt.Y)/scaleFactor)); + } + Path cleaned; + CleanPath(pth,cleaned,FINISHING_CLEAN_PATH_TOLERANCE); + AppendToolPath(output,cleaned,cleared,true); + if(pth.size()>0) { + lastPoint.X = pth[pth.size()-1].X; + lastPoint.Y = pth[pth.size()-1].Y; + } + } + + output.ReturnMotionType = CheckCollision(lastPoint, entryPoint,cleared) ? MotionType::mtLinkClear : MotionType::mtLinkNotClear; + + // dump performance results + #ifdef DEV_MODE + Perf_ProcessPolyNode.Stop(); + Perf_ProcessPolyNode.DumpResults(); + Perf_PointIterations.DumpResults(); + Perf_CalcCutArea.DumpResults(); + Perf_NextEngagePoint.DumpResults(); + Perf_ExpandCleared.DumpResults(); + Perf_DistanceToBoundary.DumpResults(); + #endif + CheckReportProgress(progressPaths, true); + #ifdef DEV_MODE + double duration=((double)(clock()-start_clock))/CLOCKS_PER_SEC; + cout<<"PolyNode perf:"<< perf_total_len/double(scaleFactor)/duration << " mm/sec" + << " processed_points:" << total_points + << " output_points:" << total_output_points + << " total_iterations:" << total_iterations + << " iter_per_point:" << (double(total_iterations)/((double(total_points)+0.001))) + << " total_exceeded:" << total_exceeded << " (" << 100 * double(total_exceeded)/double(total_points) << "%)" + << endl; + #endif + results.push_back(output); + } + +} \ No newline at end of file diff --git a/src/Mod/Path/libarea/Adaptive.hpp b/src/Mod/Path/libarea/Adaptive.hpp new file mode 100644 index 0000000000..b0df980372 --- /dev/null +++ b/src/Mod/Path/libarea/Adaptive.hpp @@ -0,0 +1,109 @@ +#include "clipper.hpp" +#include +#include + +#ifndef ADAPTIVE_HPP +#define ADAPTIVE_HPP + +//#define DEV_MODE + +#define NTOL 1.0e-7 // numeric tolerance + +namespace AdaptivePath { + using namespace ClipperLib; + + enum MotionType { mtCutting = 0, mtLinkClear = 1, mtLinkNotClear = 2, mtLinkClearAtPrevPass = 3 }; + + enum OperationType { otClearing = 0, otProfilingInside = 1, otProfilingOutside = 2 }; + + typedef std::pair DPoint; + typedef std::vector DPath; + typedef std::vector DPaths; + typedef std::pair TPath; // first parameter is MotionType, must use int due to problem with serialization to JSON in python + + // struct TPath { #this does not work correctly with pybind, changed to pair + // DPath Points; + // MotionType MType; + // }; + + typedef std::vector TPaths; + + struct AdaptiveOutput { + DPoint HelixCenterPoint; + DPoint StartPoint; + TPaths AdaptivePaths; + int ReturnMotionType; // MotionType enum, problem with serialization if enum is used + }; + + // used to isolate state -> enable potential adding of multi-threaded processing of separate regions + + class Adaptive2d { + public: + Adaptive2d(); + double toolDiameter=5; + double helixRampDiameter=0; + double stepOverFactor = 0.2; + int polyTreeNestingLimit=0; + double tolerance=0.1; + OperationType opType = OperationType::otClearing; + + std::list Execute(const DPaths &paths, std::function progressCallbackFn); + + #ifdef DEV_MODE + /*for debugging*/ + std::function DrawCircleFn; + std::function DrawPathFn; + std::function ClearScreenFn; + #endif + + private: + std::list results; + Paths inputPaths; + + double scaleFactor=100; + long toolRadiusScaled=10; + long finishPassOffsetScaled=0; + long helixRampRadiusScaled=0; + long bbox_size=0; + double referenceCutArea=0; + double optimalCutAreaPD=0; + double minCutAreaPD=0; + bool stopProcessing=false; + + time_t lastProgressTime = 0; + + std::function * progressCallback=NULL; + Path toolGeometry; // tool geometry at coord 0,0, should not be modified + + void ProcessPolyNode(Paths & boundPaths, Paths & toolBoundPaths); + bool FindEntryPoint(const Paths & toolBoundPaths,const Paths &bound, Paths &cleared /*output*/, IntPoint &entryPoint /*output*/); + double CalcCutArea(Clipper & clip,const IntPoint &toolPos, const IntPoint &newToolPos, const Paths &cleared_paths); + void AppendToolPath(AdaptiveOutput & output,const Path & passToolPath,const Paths & cleared, bool close=false); + bool CheckCollision(const IntPoint &lastPoint,const IntPoint &nextPoint,const Paths & cleared); + friend class EngagePoint; // for CalcCutArea + + void CheckReportProgress(TPaths &progressPaths,bool force=false); + + private: // constants for fine tuning + const bool preventConvetionalMode = true; + const double RESOLUTION_FACTOR = 8.0; + const int MAX_ITERATIONS = 16; + const double AREA_ERROR_FACTOR = 0.05; /* how precise to match the cut area to optimal, reasonable value: 0.05 = 5%*/ + const size_t ANGLE_HISTORY_POINTS=3; // used for angle prediction + const int DIRECTION_SMOOTHING_BUFLEN=3; // gyro points - used for angle smoothing + + const double ENGAGE_AREA_THR_FACTOR=0.2; // influences minimal engage area (factor relation to optimal) + const double ENGAGE_SCAN_DISTANCE_FACTOR=0.1; // influences the engage scan/stepping distance + + const double CLEAN_PATH_TOLERANCE = 1; + const double FINISHING_CLEAN_PATH_TOLERANCE = 0.5; + + // used for filtering out of insignificant cuts: + const double MIN_CUT_AREA_FACTOR = 0.02; // influences filtering of cuts that with cumulative area below threshold, reasonable value is between 0.01 and 0.1 + + const long PASSES_LIMIT = __LONG_MAX__; // limit used while debugging + const long POINTS_PER_PASS_LIMIT = __LONG_MAX__; // limit used while debugging + const time_t PROGRESS_TICKS = CLOCKS_PER_SEC/20; // progress report interval + }; +} +#endif \ No newline at end of file diff --git a/src/Mod/Path/libarea/CMakeLists.txt b/src/Mod/Path/libarea/CMakeLists.txt index 3084693f9b..239c5a2580 100644 --- a/src/Mod/Path/libarea/CMakeLists.txt +++ b/src/Mod/Path/libarea/CMakeLists.txt @@ -60,6 +60,7 @@ set(AREA_SRC_COMMON set(AREA_SRC_CLIPPER AreaClipper.cpp + Adaptive.cpp clipper.cpp ) @@ -99,6 +100,7 @@ add_library( ${PYAREA_SRC} ) + if(MSVC) set(area_native_LIBS debug MSVCRTD.LIB diff --git a/src/Mod/Path/libarea/PythonStuff.cpp b/src/Mod/Path/libarea/PythonStuff.cpp index 2b13c7c6e3..d98efe9b99 100644 --- a/src/Mod/Path/libarea/PythonStuff.cpp +++ b/src/Mod/Path/libarea/PythonStuff.cpp @@ -8,6 +8,7 @@ #include "Point.h" #include "AreaDxf.h" #include "kurve/geometry.h" +#include "Adaptive.hpp" #if defined (_POSIX_C_SOURCE) # undef _POSIX_C_SOURCE @@ -38,6 +39,7 @@ #include #include + #include "clipper.hpp" using namespace ClipperLib; @@ -237,7 +239,6 @@ boost::python::list spanIntersect(const Span& span1, const Span& span2) { } //Matrix(boost::python::list &l){} - boost::shared_ptr matrix_constructor(const boost::python::list& lst) { double m[16] = {1,0,0,0,0,1,0,0, 0,0,1,0, 0,0,0,1}; @@ -291,8 +292,70 @@ double AreaGetArea(const CArea& a) return a.GetArea(); } + + +// Adaptive2d.Execute wrapper +bp::list AdaptiveExecute(AdaptivePath::Adaptive2d& ada,const boost::python::list &in_paths, boost::python::object progressCallbackFn) { + bp::list out_list; + // convert inputs + AdaptivePath::DPaths dpaths; + for(bp::ssize_t i=0;i(in_paths[i]); + AdaptivePath::DPath dpath; + for(bp::ssize_t j=0;j(in_path[j]); + dpath.push_back(pair(bp::extract(in_point[0]),bp::extract(in_point[1]))); + } + dpaths.push_back(dpath); + } + // Execute with callback + std::list result=ada.Execute(dpaths,[progressCallbackFn](AdaptivePath::TPaths tp)->bool { + bp::list out_paths; + for(const auto & in_pair : tp) { + bp::list path; + for(const auto & in_pt : in_pair.second) { + path.append(bp::make_tuple(in_pt.first,in_pt.second)); + } + out_paths.append(bp::make_tuple(in_pair.first,path)); + } + return bp::extract(progressCallbackFn(out_paths)); + }); + // convert outputs back + BOOST_FOREACH(const auto & res, result) { + out_list.append(res); + } + return out_list; +} + + // Converts a std::pair instance to a Python tuple. + template + struct std_pair_to_tuple + { + static PyObject* convert(std::pair const& p) + { + return boost::python::incref( + boost::python::make_tuple(p.first, p.second).ptr()); + } + static PyTypeObject const *get_pytype () { + return &PyTuple_Type; + } + }; + + boost::python::list AdaptiveOutput_AdaptivePaths(const AdaptivePath::AdaptiveOutput &ado) { + bp::list olist; + for(auto & ap : ado.AdaptivePaths) { + bp::list op; + for(auto & pt : ap.second) { + op.append(bp::make_tuple(pt.first, pt.second)); + } + olist.append(bp::make_tuple(ap.first, op)); + } + return olist; + } + + BOOST_PYTHON_MODULE(area) { - bp::class_("Point") + bp::class_("Point") .def(bp::init()) .def(bp::init()) .def(bp::other() * bp::self) @@ -418,4 +481,43 @@ BOOST_PYTHON_MODULE(area) { bp::def("holes_linked", holes_linked); bp::def("AreaFromDxf", AreaFromDxf); bp::def("TangentialArc", TangentialArc); + + + using namespace AdaptivePath; + + boost::python::to_python_converter, std_pair_to_tuple,true>(); + + + bp::enum_("AdaptiveMotionType") + .value("Cutting", MotionType::mtCutting) + .value("LinkClear", MotionType::mtLinkClear) + .value("LinkNotClear", MotionType::mtLinkNotClear) + .value("LinkClearAtPrevPass", MotionType::mtLinkClearAtPrevPass); + + bp::enum_("AdaptiveOperationType") + .value("Clearing", OperationType::otClearing) + .value("ProfilingInside", OperationType::otProfilingInside) + .value("ProfilingOutside", OperationType::otProfilingOutside); + + bp::class_ ("AdaptiveOutput") + .def(bp::init<>()) + .add_property("HelixCenterPoint", bp::make_getter(&AdaptiveOutput::HelixCenterPoint, bp::return_value_policy())) + .add_property("StartPoint", bp::make_getter(&AdaptiveOutput::StartPoint, bp::return_value_policy())) + .add_property("AdaptivePaths", &AdaptiveOutput_AdaptivePaths) + .def_readonly("ReturnMotionType",&AdaptiveOutput::ReturnMotionType); + + bp::class_("Adaptive2d") + .def(bp::init<>()) + .def("Execute",&AdaptiveExecute) + .def_readwrite("stepOverFactor", &Adaptive2d::stepOverFactor) + .def_readwrite("toolDiameter", &Adaptive2d::toolDiameter) + .def_readwrite("helixRampDiameter", &Adaptive2d::helixRampDiameter) + .def_readwrite("polyTreeNestingLimit", &Adaptive2d::polyTreeNestingLimit) + .def_readwrite("tolerance", &Adaptive2d::tolerance) + .def_readwrite("opType", &Adaptive2d::opType); + + } + + + diff --git a/src/Mod/Path/libarea/pyarea.cpp b/src/Mod/Path/libarea/pyarea.cpp index 1b5fd92630..87af8e68de 100644 --- a/src/Mod/Path/libarea/pyarea.cpp +++ b/src/Mod/Path/libarea/pyarea.cpp @@ -11,6 +11,7 @@ #include "Point.h" #include "AreaDxf.h" #include "kurve/geometry.h" +#include "Adaptive.hpp" #include #include @@ -358,9 +359,46 @@ void init_pyarea(py::module &m){ m.def("holes_linked", holes_linked); m.def("AreaFromDxf", AreaFromDxf); m.def("TangentialArc", TangentialArc); + + using namespace AdaptivePath; + py::enum_(m, "AdaptiveMotionType") + .value("Cutting", MotionType::mtCutting) + .value("LinkClear", MotionType::mtLinkClear) + .value("LinkNotClear", MotionType::mtLinkNotClear) + .value("LinkClearAtPrevPass", MotionType::mtLinkClearAtPrevPass); + + py::enum_(m, "AdaptiveOperationType") + .value("Clearing", OperationType::otClearing) + .value("ProfilingInside", OperationType::otProfilingInside) + .value("ProfilingOutside", OperationType::otProfilingOutside); + + py::class_(m, "AdaptiveOutput") + .def(py::init<>()) + .def_readwrite("HelixCenterPoint",&AdaptiveOutput::HelixCenterPoint) + .def_readwrite("StartPoint",&AdaptiveOutput::StartPoint) + .def_readwrite("AdaptivePaths",&AdaptiveOutput::AdaptivePaths) + .def_readwrite("ReturnMotionType",&AdaptiveOutput::ReturnMotionType); + + py::class_(m, "Adaptive2d") + .def(py::init<>()) + .def("Execute",&Adaptive2d::Execute) + .def_readwrite("stepOverFactor", &Adaptive2d::stepOverFactor) + .def_readwrite("toolDiameter", &Adaptive2d::toolDiameter) + .def_readwrite("helixRampDiameter", &Adaptive2d::helixRampDiameter) + .def_readwrite("polyTreeNestingLimit", &Adaptive2d::polyTreeNestingLimit) + .def_readwrite("tolerance", &Adaptive2d::tolerance) + .def_readwrite("opType", &Adaptive2d::opType); } PYBIND11_MODULE(area, m){ m.doc()= "not yet"; init_pyarea(m); }; + + + + +using namespace AdaptivePath; +PYBIND11_MODULE(AdaptivePath, m){ + +} \ No newline at end of file