diff --git a/src/Mod/Path/App/Path.cpp b/src/Mod/Path/App/Path.cpp
index c802a61768..e0451ff0a5 100644
--- a/src/Mod/Path/App/Path.cpp
+++ b/src/Mod/Path/App/Path.cpp
@@ -31,6 +31,7 @@
#include
#include
#include
+#include
// KDL stuff - at the moment, not used
//#include "Mod/Robot/App/kdl_cp/path_line.hpp"
@@ -146,6 +147,67 @@ double Toolpath::getLength()
return l;
}
+double Toolpath::getCycleTime(double hFeed, double vFeed, double hRapid, double vRapid)
+{
+ // check the feedrates are set
+ if ((hFeed == 0) || (vFeed == 0)){
+ Base::Console().Warning("Feed Rate Error: Check Tool Controllers have Feed Rates");
+ return 0;
+ }
+
+ if (hRapid == 0){
+ hRapid = hFeed;
+ }
+
+ if (vRapid == 0){
+ vRapid = vFeed;
+ }
+
+ if(vpcCommands.size()==0)
+ return 0;
+ double l = 0;
+ double time = 0;
+ bool verticalMove = false;
+ Vector3d last(0,0,0);
+ Vector3d next;
+ for(std::vector::const_iterator it = vpcCommands.begin();it!=vpcCommands.end();++it) {
+ std::string name = (*it)->Name;
+ float feedrate = (*it)->getParam("F");
+
+ l = 0;
+ verticalMove = false;
+ feedrate = hFeed;
+ next = (*it)->getPlacement(last).getPosition();
+
+ if (last.z != next.z){
+ verticalMove = true;
+ feedrate = vFeed;
+ }
+
+ if ((name == "G0") || (name == "G00")){
+ // Rapid Move
+ l += (next - last).Length();
+ feedrate = hRapid;
+ if(verticalMove){
+ feedrate = vRapid;
+ }
+ }else if ((name == "G1") || (name == "G01")) {
+ // Feed Move
+ l += (next - last).Length();
+ }else if ((name == "G2") || (name == "G02") || (name == "G3") || (name == "G03") ) {
+ // Arc Move
+ Vector3d center = (*it)->getCenter();
+ double radius = (last - center).Length();
+ double angle = (next - center).GetAngle(last - center);
+ l += angle * radius;
+ }
+
+ time += l / feedrate;
+ last = next;
+ }
+ return time;
+}
+
class BoundBoxSegmentVisitor : public PathSegmentVisitor
{
public:
diff --git a/src/Mod/Path/App/Path.h b/src/Mod/Path/App/Path.h
index 24e1023312..9ad4381f7b 100644
--- a/src/Mod/Path/App/Path.h
+++ b/src/Mod/Path/App/Path.h
@@ -60,6 +60,7 @@ namespace Path
void insertCommand(const Command &Cmd, int); // inserts a command
void deleteCommand(int); // deletes a command
double getLength(void); // return the Length (mm) of the Path
+ double getCycleTime(double, double, double, double); // return the Cycle Time (s) of the Path
void recalculate(void); // recalculates the points
void setFromGCode(const std::string); // sets the path from the contents of the given GCode string
std::string toGCode(void) const; // gets a gcode string representation from the Path
diff --git a/src/Mod/Path/App/PathPy.xml b/src/Mod/Path/App/PathPy.xml
index 4440ea2379..94a56ec172 100644
--- a/src/Mod/Path/App/PathPy.xml
+++ b/src/Mod/Path/App/PathPy.xml
@@ -78,6 +78,11 @@ deletes the command found at the given position or from the end of the pathreturns a copy of this path
+
+
+ return the cycle time estimation for this path in s
+
+
diff --git a/src/Mod/Path/App/PathPyImp.cpp b/src/Mod/Path/App/PathPyImp.cpp
index 20bac63a8f..f61c5568e6 100644
--- a/src/Mod/Path/App/PathPyImp.cpp
+++ b/src/Mod/Path/App/PathPyImp.cpp
@@ -189,6 +189,14 @@ PyObject* PathPy::deleteCommand(PyObject * args)
Py_Error(Base::BaseExceptionFreeCADError, "Wrong parameters - expected an integer (optional)");
}
+PyObject* PathPy::getCycleTime(PyObject * args)
+{
+ double hFeed, vFeed, hRapid, vRapid;
+ if (PyArg_ParseTuple(args, "dddd", &hFeed, &vFeed, &hRapid, &vRapid)){
+ return PyFloat_FromDouble(getToolpathPtr()->getCycleTime(hFeed, vFeed, hRapid, vRapid));
+ }
+}
+
// GCode methods
PyObject* PathPy::toGCode(PyObject * args)
diff --git a/src/Mod/Path/PathScripts/PathJob.py b/src/Mod/Path/PathScripts/PathJob.py
index 2225c2fa9f..463a36deab 100644
--- a/src/Mod/Path/PathScripts/PathJob.py
+++ b/src/Mod/Path/PathScripts/PathJob.py
@@ -30,7 +30,7 @@ import PathScripts.PathSetupSheet as PathSetupSheet
import PathScripts.PathStock as PathStock
import PathScripts.PathToolController as PathToolController
import PathScripts.PathUtil as PathUtil
-import json
+import json, time
# lazily loaded modules
from lazy_loader.lazy_loader import LazyLoader
@@ -99,6 +99,8 @@ class ObjectJob:
obj.addProperty("App::PropertyString", "PostProcessorArgs", "Output", QtCore.QT_TRANSLATE_NOOP("PathJob", "Arguments for the Post Processor (specific to the script)"))
obj.addProperty("App::PropertyString", "Description", "Path", QtCore.QT_TRANSLATE_NOOP("PathJob","An optional description for this job"))
+ obj.addProperty("App::PropertyString", "CycleTime", "Path", QtCore.QT_TRANSLATE_NOOP("PathOp", "Operations Cycle Time Estimation"))
+ obj.setEditorMode('CycleTime', 1) # read-only
obj.addProperty("App::PropertyDistance", "GeometryTolerance", "Geometry", QtCore.QT_TRANSLATE_NOOP("PathJob", "For computing Paths; smaller increases accuracy, but slows down computation"))
obj.addProperty("App::PropertyLink", "Stock", "Base", QtCore.QT_TRANSLATE_NOOP("PathJob", "Solid object to be used as stock."))
@@ -110,7 +112,7 @@ class ObjectJob:
obj.addProperty("App::PropertyStringList", "Fixtures", "WCS", QtCore.QT_TRANSLATE_NOOP("PathJob", "The Work Coordinate Systems for the Job"))
obj.OrderOutputBy = ['Fixture', 'Tool', 'Operation']
obj.Fixtures = ['G54']
-
+
obj.PostProcessorOutputFile = PathPreferences.defaultOutputFile()
#obj.setEditorMode("PostProcessorOutputFile", 0) # set to default mode
obj.PostProcessor = postProcessors = PathPreferences.allEnabledPostProcessors()
@@ -252,6 +254,10 @@ class ObjectJob:
obj.setEditorMode('Operations', 2) # hide
obj.setEditorMode('Placement', 2)
+ if not hasattr(obj, 'CycleTime'):
+ obj.addProperty("App::PropertyString", "CycleTime", "Path", QtCore.QT_TRANSLATE_NOOP("PathOp", "Operations Cycle Time Estimation"))
+ obj.setEditorMode('CycleTime', 1) # read-only
+
def onChanged(self, obj, prop):
if prop == "PostProcessor" and obj.PostProcessor:
processor = PostProcessor.load(obj.PostProcessor)
@@ -342,6 +348,37 @@ class ObjectJob:
def execute(self, obj):
obj.Path = obj.Operations.Path
+ self.getCycleTime()
+
+ def getCycleTime(self):
+ seconds = 0
+ for op in self.obj.Operations.Group:
+
+ # Skip inactive operations
+ if PathUtil.opProperty(op, 'Active') is False:
+ continue
+
+ # Skip operations that don't have a cycletime attribute
+ if not PathUtil.opProperty(op, 'CycleTime') or PathUtil.opProperty(op, 'CycleTime') is None:
+ continue
+
+ formattedCycleTime = PathUtil.opProperty(op, 'CycleTime')
+ try:
+ ## convert the formatted time from HH:MM:SS to just seconds
+ opCycleTime = sum(x * int(t) for x, t in zip([1, 60, 3600], reversed(formattedCycleTime.split(":"))))
+ except:
+ FreeCAD.Console.PrintWarning("Error converting the operations cycle time. Job Cycle time may be innacturate\n")
+ continue
+
+ if opCycleTime > 0:
+ seconds = seconds + opCycleTime
+
+ if seconds > 0:
+ cycleTimeString = time.strftime("%H:%M:%S", time.gmtime(seconds))
+ else:
+ cycleTimeString = translate('PathGui', 'Cycle Time Error')
+
+ self.obj.CycleTime = cycleTimeString
def addOperation(self, op, before = None, removeBefore = False):
group = self.obj.Operations.Group
diff --git a/src/Mod/Path/PathScripts/PathOp.py b/src/Mod/Path/PathScripts/PathOp.py
index fab24c28c2..97a8f0fa19 100644
--- a/src/Mod/Path/PathScripts/PathOp.py
+++ b/src/Mod/Path/PathScripts/PathOp.py
@@ -31,6 +31,7 @@ import PathScripts.PathUtils as PathUtils
from PathScripts.PathUtils import waiting_effects
from PySide import QtCore
+import time
# lazily loaded modules
from lazy_loader.lazy_loader import LazyLoader
@@ -122,6 +123,8 @@ class ObjectOp(object):
obj.addProperty("App::PropertyBool", "Active", "Path", QtCore.QT_TRANSLATE_NOOP("PathOp", "Make False, to prevent operation from generating code"))
obj.addProperty("App::PropertyString", "Comment", "Path", QtCore.QT_TRANSLATE_NOOP("PathOp", "An optional comment for this Operation"))
obj.addProperty("App::PropertyString", "UserLabel", "Path", QtCore.QT_TRANSLATE_NOOP("PathOp", "User Assigned Label"))
+ obj.addProperty("App::PropertyString", "CycleTime", "Path", QtCore.QT_TRANSLATE_NOOP("PathOp", "Operations Cycle Time Estimation"))
+ obj.setEditorMode('CycleTime', 1) # read-only
features = self.opFeatures(obj)
@@ -189,7 +192,7 @@ class ObjectOp(object):
def setEditorModes(self, obj, features):
'''Editor modes are not preserved during document store/restore, set editor modes for all properties'''
- for op in ['OpStartDepth', 'OpFinalDepth', 'OpToolDiameter']:
+ for op in ['OpStartDepth', 'OpFinalDepth', 'OpToolDiameter', 'CycleTime']:
if hasattr(obj, op):
obj.setEditorMode(op, 1) # read-only
@@ -226,6 +229,9 @@ class ObjectOp(object):
obj.addProperty("App::PropertyEnumeration", "EnableRotation", "Rotation", QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable rotation to gain access to pockets/areas not normal to Z axis."))
obj.EnableRotation = ['Off', 'A(x)', 'B(y)', 'A & B']
+ if not hasattr(obj, 'CycleTime'):
+ obj.addProperty("App::PropertyString", "CycleTime", "Path", QtCore.QT_TRANSLATE_NOOP("PathOp", "Operations Cycle Time Estimation"))
+
self.setEditorModes(obj, features)
self.opOnDocumentRestored(obj)
@@ -521,8 +527,41 @@ class ObjectOp(object):
path = Path.Path(self.commandlist)
obj.Path = path
+ obj.CycleTime = self.getCycleTimeEstimate(obj)
+ self.job.Proxy.getCycleTime()
return result
+ def getCycleTimeEstimate(self, obj):
+
+ tc = obj.ToolController
+
+ if tc is None or tc.ToolNumber == 0:
+ FreeCAD.Console.PrintError("No Tool Controller is selected. Tool feed rates required to calculate the cycle time.\n")
+ return translate('PathGui', 'Tool Error')
+
+ hFeedrate = tc.HorizFeed.Value
+ vFeedrate = tc.VertFeed.Value
+ hRapidrate = tc.HorizRapid.Value
+ vRapidrate = tc.VertRapid.Value
+
+ if hFeedrate == 0 or vFeedrate == 0:
+ FreeCAD.Console.PrintError("Tool Controller requires feed rates. Tool feed rates required to calculate the cycle time.\n")
+ return translate('PathGui', 'Feedrate Error')
+
+ if hRapidrate == 0 or vRapidrate == 0:
+ FreeCAD.Console.PrintWarning("Add Tool Controller Rapid Speeds on the SetupSheet for more accurate cycle times.\n")
+
+ ## get the cycle time in seconds
+ seconds = obj.Path.getCycleTime(hFeedrate, vFeedrate, hRapidrate, vRapidrate)
+
+ if not seconds:
+ return translate('PathGui', 'Cycletime Error')
+
+ ## convert the cycle time to a HH:MM:SS format
+ cycleTime = time.strftime("%H:%M:%S", time.gmtime(seconds))
+
+ return cycleTime
+
def addBase(self, obj, base, sub):
PathLog.track(obj, base, sub)
base = PathUtil.getPublicObject(base)