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