Merge pull request #859 from sliptonic/master

Fixes for startpoint.
This commit is contained in:
Yorik van Havre
2017-07-08 12:20:28 -03:00
committed by GitHub
17 changed files with 697 additions and 313 deletions

View File

@@ -122,7 +122,7 @@ public:
"fromShapes(shapes, start=Vector(), return_end=False" PARAM_PY_ARGS_DOC(ARG,AREA_PARAMS_PATH) ")\n"
"\nReturns a Path object from a list of shapes\n"
"\n* shapes: input list of shapes.\n"
"\n* start (Vector()): optional start position.\n"
"\n* start (Vector()): feed start position, and also serves as a hint of path entry.\n"
"\n* return_end (False): if True, returns tuple (path, endPosition).\n"
PARAM_PY_DOC(ARG, AREA_PARAMS_PATH)
);
@@ -415,7 +415,7 @@ private:
try {
bool need_arc_plane = arc_plane==Area::ArcPlaneAuto;
std::list<TopoDS_Shape> wires = Area::sortWires(shapes,&pstart,
&pend, &arc_plane, PARAM_PY_FIELDS(PARAM_FARG,AREA_PARAMS_SORT));
&pend, 0, &arc_plane, PARAM_PY_FIELDS(PARAM_FARG,AREA_PARAMS_SORT));
PyObject *list = PyList_New(0);
for(auto &wire : wires)
PyList_Append(list,Py::new_reference_to(

View File

@@ -285,6 +285,11 @@ void Area::addWire(CArea &area, const TopoDS_Wire& wire,
BRepTools_WireExplorer xp(trsf?TopoDS::Wire(
wire.Moved(TopLoc_Location(*trsf))):wire);
if(!xp.More()) {
AREA_TRACE("empty wire");
return;
}
gp_Pnt p = BRep_Tool::Pnt(xp.CurrentVertex());
ccurve.append(CVertex(Point(p.X(),p.Y())));
@@ -2060,13 +2065,14 @@ TopoDS_Shape Area::toShape(const CArea &area, bool fill, const gp_Trsf *trsf, in
struct WireInfo {
TopoDS_Wire wire;
std::deque<gp_Pnt> points;
gp_Pnt pt_end;
bool isClosed;
inline const gp_Pnt &pstart() const{
return points.front();
}
inline const gp_Pnt &pend() const{
return isClosed?pstart():points.back();
return isClosed?pstart():pt_end;
}
};
@@ -2156,8 +2162,9 @@ struct GetWires {
// We don't add in-between vertices of an open wire, because we
// haven't implemented open wire breaking yet.
info.points.push_back(p1);
if(params.direction!=Area::DirectionNone)
if(!info.isClosed && params.direction==Area::DirectionNone)
info.points.push_back(p2);
info.pt_end = p2;
} else {
// For closed wires, we are can easily rebase the wire, so we
// discretize the wires to spatial index it in order to accelerate
@@ -2188,7 +2195,7 @@ struct GetWires {
}
auto it = wires.end();
--it;
for(size_t i=0,count=info.points.size();i<count;++i)
for(size_t i=0,count=it->points.size();i<count;++i)
rtree.insert(RValue(it,i));
FC_DURATION_PLUS(params.bd,t);
}
@@ -2267,7 +2274,7 @@ struct ShapeInfo{
}
if(!done){
double d1 = pt.SquareDistance(it->pstart());
if(myParams.direction==Area::DirectionNone) {
if(myParams.direction!=Area::DirectionNone) {
d = d1;
p = it->pstart();
is_start = true;
@@ -2430,11 +2437,14 @@ struct ShapeInfo{
return myBestWire->wire;
}
std::list<TopoDS_Shape> sortWires(const gp_Pnt &pstart, gp_Pnt &pend,double min_dist) {
std::list<TopoDS_Shape> sortWires(const gp_Pnt &pstart, gp_Pnt &pend,double min_dist, gp_Pnt *pentry) {
if(pstart.SquareDistance(myStartPt)>Precision::SquareConfusion())
if(myWires.empty() ||
pstart.SquareDistance(myStartPt)>Precision::SquareConfusion())
nearest(pstart);
if(pentry) *pentry = myBestPt;
std::list<TopoDS_Shape> wires;
if(min_dist < 0.01)
min_dist = 0.01;
@@ -2602,7 +2612,7 @@ struct WireOrienter {
};
std::list<TopoDS_Shape> Area::sortWires(const std::list<TopoDS_Shape> &shapes,
const gp_Pnt *_pstart, gp_Pnt *_pend, short *_parc_plane,
gp_Pnt *_pstart, gp_Pnt *_pend, double *stepdown_hint, short *_parc_plane,
PARAM_ARGS(PARAM_FARG,AREA_PARAMS_SORT))
{
std::list<TopoDS_Shape> wires;
@@ -2727,7 +2737,12 @@ std::list<TopoDS_Shape> Area::sortWires(const std::list<TopoDS_Shape> &shapes,
AREA_TRACE("bound (" << xMin<<", "<<xMax<<"), ("<<
yMin<<", "<<yMax<<"), ("<<zMin<<", "<<zMax<<')');
pstart.SetCoord(xMax,yMax,zMax);
if(_pstart) *_pstart = pstart;
}
gp_Pln pln;
double hint = 0.0;
bool hint_first = true;
while(shape_list.size()) {
AREA_TRACE("sorting " << shape_list.size() << ' ' << AREA_XYZ(pstart));
double best_d;
@@ -2746,10 +2761,42 @@ std::list<TopoDS_Shape> Area::sortWires(const std::list<TopoDS_Shape> &shapes,
best_d = d;
}
}
wires.splice(wires.end(),best_it->sortWires(pstart,pend,min_dist));
gp_Pnt pentry;
wires.splice(wires.end(),best_it->sortWires(pstart,pend,min_dist,&pentry));
if(use_bound && _pstart) {
use_bound = false;
*_pstart = pentry;
}
if(sort_mode==SortMode2D5 && stepdown_hint) {
if(!best_it->myPlanar)
hint_first = true;
else if(hint_first)
hint_first = false;
else{
// Calculate distance of two gp_pln.
//
// Can't use gp_pln.Distance(), because it only calculate
// the distance if two plane are parallel. And it checks
// parallelity using tolerance gp::Resolution() which is
// defined as DBL_MIN (min double) in Standard_Real.hxx.
// Really? Is that a bug?
const gp_Pnt& P = pln.Position().Location();
const gp_Pnt& loc = best_it->myPln.Position().Location ();
const gp_Dir& dir = best_it->myPln.Position().Direction();
double d = (dir.X() * (P.X() - loc.X()) +
dir.Y() * (P.Y() - loc.Y()) +
dir.Z() * (P.Z() - loc.Z()));
if (d < 0) d = -d;
if(d>hint)
hint = d;
}
pln = best_it->myPln;
}
pstart = pend;
shape_list.erase(best_it);
}
if(stepdown_hint && hint!=0.0)
*stepdown_hint = hint;
if(_pend) *_pend = pend;
FC_DURATION_LOG(rparams.bd,"rtree build");
FC_DURATION_LOG(rparams.qd,"rtree query");
@@ -2800,24 +2847,22 @@ static void addG0(bool verbose, Toolpath &path,
double retraction, double resume_height,
double f, double &last_f)
{
if(!getter || retraction-(last.*getter)() < Precision::Confusion()) {
addGCode(verbose,path,last,next,"G0");
return;
}
gp_Pnt pt(last);
(pt.*setter)(retraction);
addGCode(verbose,path,last,pt,"G0");
last = pt;
pt = next;
(pt.*setter)(retraction);
addGCode(verbose,path,last,pt,"G0");
if(resume_height>Precision::Confusion() &&
resume_height+(next.*getter)() < retraction)
{
if(retraction-(last.*getter)() > Precision::Confusion()) {
(pt.*setter)(retraction);
addGCode(verbose,path,last,pt,"G0");
last = pt;
pt = next;
(pt.*setter)((next.*getter)()+resume_height);
(pt.*setter)(retraction);
addGCode(verbose,path,last,pt,"G0");
}
if(resume_height>Precision::Confusion()) {
if(resume_height+(next.*getter)() < retraction) {
last = pt;
pt = next;
(pt.*setter)((next.*getter)()+resume_height);
addGCode(verbose,path,last,pt,"G0");
}
addG1(verbose,path,pt,next,f,last_f);
}else
addGCode(verbose,path,pt,next,"G0");
@@ -2875,11 +2920,15 @@ void Area::setWireOrientation(TopoDS_Wire &wire, const gp_Dir &dir, bool wire_cc
}
void Area::toPath(Toolpath &path, const std::list<TopoDS_Shape> &shapes,
const gp_Pnt *pstart, gp_Pnt *pend, PARAM_ARGS(PARAM_FARG,AREA_PARAMS_PATH))
const gp_Pnt *_pstart, gp_Pnt *pend, PARAM_ARGS(PARAM_FARG,AREA_PARAMS_PATH))
{
std::list<TopoDS_Shape> wires;
wires = sortWires(shapes,pstart,pend,
gp_Pnt pstart;
if(_pstart) pstart = *_pstart;
double stepdown_hint = 1.0;
wires = sortWires(shapes,&pstart,pend,&stepdown_hint,
PARAM_REF(PARAM_FARG,AREA_PARAMS_ARC_PLANE),
PARAM_FIELDS(PARAM_FARG,AREA_PARAMS_SORT));
@@ -2898,30 +2947,57 @@ void Area::toPath(Toolpath &path, const std::list<TopoDS_Shape> &shapes,
addGCode(path,"G17");
}
AxisGetter getter;
AxisSetter setter;
switch(retract_axis) {
case RetractAxisX:
getter = &gp_Pnt::X;
setter = &gp_Pnt::SetX;
break;
case RetractAxisY:
getter = &gp_Pnt::Y;
setter = &gp_Pnt::SetY;
break;
default:
getter = &gp_Pnt::Z;
setter = &gp_Pnt::SetZ;
}
threshold = fabs(threshold);
if(threshold < Precision::Confusion())
threshold = Precision::Confusion();
threshold *= threshold;
resume_height = fabs(resume_height);
AxisGetter getter = &gp_Pnt::Z;
AxisSetter setter = &gp_Pnt::SetZ;
resume_height = fabs(resume_height);
if(resume_height < Precision::Confusion())
resume_height = stepdown_hint;
retraction = fabs(retraction);
if(retraction>Precision::Confusion()) {
switch(retract_axis) {
case RetractAxisX:
getter = &gp_Pnt::X;
setter = &gp_Pnt::SetX;
break;
case RetractAxisY:
getter = &gp_Pnt::Y;
setter = &gp_Pnt::SetY;
break;
}
}
if(retraction < Precision::Confusion())
retraction = (pstart.*getter)()+resume_height;
// in case the user didn't specify feed start, sortWire() will choose one
// based on the bound. We'll further adjust that according to resume height
if(!_pstart || pstart.SquareDistance(*_pstart)>Precision::SquareConfusion())
(pstart.*setter)((pstart.*getter)()+resume_height);
gp_Pnt plast,p;
if(pstart) plast = *pstart;
// initial vertial rapid pull up to retraction (or start Z height if higher)
(p.*setter)(std::max(retraction,(pstart.*getter)()));
addGCode(false,path,plast,p,"G0");
plast = p;
p = pstart;
// rapid horizontal move if start Z is below retraction
if(fabs((p.*getter)()-retraction) > Precision::Confusion()) {
(p.*setter)(retraction);
addGCode(false,path,plast,p,"G0");
plast = p;
p = pstart;
}
// vertial rapid down to feed start
addGCode(false,path,plast,p,"G0");
plast = p;
bool first = true;
bool arcWarned = false;
double cur_f = 0.0; // current feed rate
@@ -2942,7 +3018,7 @@ void Area::toPath(Toolpath &path, const std::list<TopoDS_Shape> &shapes,
(pTmp.*setter)(0.0);
(plastTmp.*setter)(0.0);
if(first||pTmp.SquareDistance(plastTmp)>threshold)
if(!first && pTmp.SquareDistance(plastTmp)>threshold)
addG0(verbose,path,plast,p,getter,setter,retraction,resume_height,vf,cur_f);
else
addG1(verbose,path,plast,p,vf,cur_f);

View File

@@ -341,6 +341,8 @@ public:
* \arg \c shapes: input list of shapes.
* \arg \c pstart: optional start point
* \arg \c pend: optional output containing the ending point of the returned
* \arg \c stepdown_hint: optional output of a hint of step down as the max
* distance between two sections.
* \arg \c arc_plane: optional arc plane selection, if given the found plane
* will be returned. See #AREA_PARAMS_ARC_PLANE for more details.
*
@@ -349,8 +351,8 @@ public:
* \return sorted wires
*/
static std::list<TopoDS_Shape> sortWires(const std::list<TopoDS_Shape> &shapes,
const gp_Pnt *pstart=NULL, gp_Pnt *pend=NULL, short *arc_plane = NULL,
PARAM_ARGS_DEF(PARAM_FARG,AREA_PARAMS_SORT));
gp_Pnt *pstart=NULL, gp_Pnt *pend=NULL, double *stepdown_hint=NULL,
short *arc_plane = NULL, PARAM_ARGS_DEF(PARAM_FARG,AREA_PARAMS_SORT));
/** Convert a list of wires to gcode
*

View File

@@ -55,7 +55,7 @@ PARAM_ENUM_STRING_DECLARE(static const char *Enums,AREA_PARAMS_PATH)
FeatureShape::FeatureShape()
{
ADD_PROPERTY(Sources,(0));
ADD_PROPERTY_TYPE(StartPoint,(Base::Vector3d()),"Path",App::Prop_None,"Path start position");
ADD_PROPERTY_TYPE(StartPoint,(Base::Vector3d()),"Path",App::Prop_None,"Feed start position");
PARAM_PROP_ADD("Path",AREA_PARAMS_PATH);
PARAM_PROP_SET_ENUM(Enums,AREA_PARAMS_PATH);
}

View File

@@ -65,6 +65,9 @@ SET(PathScripts_SRCS
PathScripts/PostUtils.py
PathScripts/__init__.py
PathScripts/kdtree.py
)
SET(PathScripts_post_SRCS
PathScripts/post/__init__.py
PathScripts/post/centroid_post.py
PathScripts/post/comparams_post.py
@@ -84,7 +87,9 @@ SET(PathScripts_SRCS
SET(PathTests_SRCS
PathTests/__init__.py
PathTests/boxtest.fcstd
PathTests/PathTestUtils.py
PathTests/test_centroid_00.ngc
PathTests/test_linuxcnc_00.ngc
PathTests/TestPathCore.py
PathTests/TestPathDepthParams.py
@@ -97,7 +102,7 @@ SET(PathTests_SRCS
SET(all_files
${PathScripts_SRCS}
${PathScripts_NC_SRCS}
${PathScripts_post_SRCS}
)
ADD_CUSTOM_TARGET(PathScripts ALL
@@ -129,3 +134,10 @@ INSTALL(
DESTINATION
Mod/Path/PathTests
)
INSTALL(
FILES
${PathScripts_post_SRCS}
DESTINATION
Mod/Path/PathScripts/post
)

View File

@@ -45,6 +45,7 @@ else:
if FreeCAD.GuiUp:
import FreeCADGui
# Qt tanslation handling
def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
@@ -62,7 +63,6 @@ class ObjectContour:
PathLog.track()
obj.addProperty("App::PropertyBool", "Active", "Path", QtCore.QT_TRANSLATE_NOOP("App::Property", "Make False, to prevent operation from generating code"))
obj.addProperty("App::PropertyString", "Comment", "Path", QtCore.QT_TRANSLATE_NOOP("App::Property", "An optional comment for this Contour"))
#obj.addProperty("App::PropertyString", "UserLabel", "Path", QtCore.QT_TRANSLATE_NOOP("App::Property", "User Assigned Label"))
# Tool Properties
obj.addProperty("App::PropertyLink", "ToolController", "Path", QtCore.QT_TRANSLATE_NOOP("App::Property", "The tool controller that will be used to calculate the path"))
@@ -76,6 +76,7 @@ class ObjectContour:
# Start Point Properties
obj.addProperty("App::PropertyVector", "StartPoint", "Start Point", QtCore.QT_TRANSLATE_NOOP("App::Property", "The start point of this path"))
obj.addProperty("App::PropertyBool", "UseStartPoint", "Start Point", QtCore.QT_TRANSLATE_NOOP("App::Property", "make True, if specifying a Start Point"))
# Contour Properties
obj.addProperty("App::PropertyEnumeration", "Direction", "Contour", QtCore.QT_TRANSLATE_NOOP("App::Property", "The direction that the toolpath should go around the part ClockWise CW or CounterClockWise CCW"))
@@ -85,8 +86,12 @@ class ObjectContour:
obj.addProperty("App::PropertyDistance", "OffsetExtra", "Contour", QtCore.QT_TRANSLATE_NOOP("App::Property", "Extra value to stay away from final Contour- good for roughing toolpath"))
# Debug Parameters
# obj.addProperty("App::PropertyString", "AreaParams", "Debug", QtCore.QT_TRANSLATE_NOOP("App::Property", "parameters used by PathArea"))
# obj.setEditorMode('AreaParams', 2) # hide
obj.addProperty("App::PropertyString", "AreaParams", "Path")
obj.setEditorMode('AreaParams', 2) # hide
obj.addProperty("App::PropertyString", "PathParams", "Path")
obj.setEditorMode('PathParams', 2) # hide
obj.addProperty("Part::PropertyPartShape", "removalshape", "Path")
obj.setEditorMode('removalshape', 2) # hide
if FreeCAD.GuiUp:
_ViewProviderContour(obj.ViewObject)
@@ -96,7 +101,8 @@ class ObjectContour:
def onChanged(self, obj, prop):
PathLog.track('prop: {} state: {}'.format(prop, obj.State))
#pass
if prop in ['AreaParams', 'PathParams', 'removalshape']:
obj.setEditorMode(prop, 2)
def __getstate__(self):
PathLog.track()
@@ -145,7 +151,7 @@ class ObjectContour:
heights = [i for i in self.depthparams]
PathLog.debug('depths: {}'.format(heights))
profile.setParams(**profileparams)
#obj.AreaParams = str(profile.getParams())
obj.AreaParams = str(profile.getParams())
PathLog.debug("Contour with params: {}".format(profile.getParams()))
sections = profile.makeSections(mode=0, project=True, heights=heights)
@@ -166,21 +172,22 @@ class ObjectContour:
if self.endVector is not None:
params['start'] = self.endVector
elif start is not None:
params['start'] = start
elif obj.UseStartPoint:
params['start'] = obj.StartPoint
obj.PathParams = str({key: value for key, value in params.items() if key != 'shapes'})
(pp, end_vector) = Path.fromShapes(**params)
PathLog.debug("Generating Path with params: {}".format(params))
PathLog.debug('pp: {}, end vector: {}'.format(pp, end_vector))
self.endVector = end_vector
simobj = None
if getsim:
profileparams['Thicken'] = True #{'Fill':0, 'Coplanar':0, 'Project':True, 'SectionMode':2, 'Thicken':True}
profileparams['ToolRadius']= self.radius - self.radius *.005
profileparams['Thicken'] = True
profileparams['ToolRadius'] = self.radius - self.radius * .005
profile.setParams(**profileparams)
sec = profile.makeSections(mode=0, project=False, heights=heights)[-1].getShape()
simobj = sec.extrude(FreeCAD.Vector(0,0,baseobject.BoundBox.ZMax))
simobj = sec.extrude(FreeCAD.Vector(0, 0, baseobject.BoundBox.ZMax))
return pp, simobj
@@ -207,6 +214,7 @@ class ObjectContour:
user_depths=None)
if toolLoad is None or toolLoad.ToolNumber == 0:
FreeCAD.Console.PrintError("No Tool Controller is selected. We need a tool to build a Path.")
return
else:
@@ -229,15 +237,13 @@ class ObjectContour:
commandlist.append(Path.Command("(Uncompensated Tool Path)"))
parentJob = PathUtils.findParentJob(obj)
if parentJob is None:
return
baseobject = parentJob.Base
if baseobject is None:
return
# Let's always start by rapid to clearance...just for safety
commandlist.append(Path.Command("G0", {"Z": obj.ClearanceHeight.Value}))
isPanel = False
if hasattr(baseobject, "Proxy"):
if isinstance(baseobject.Proxy, ArchPanel.PanelSheet): # process the sheet
@@ -256,10 +262,9 @@ class ObjectContour:
FreeCAD.Console.PrintError("Something unexpected happened. Unable to generate a contour path. Check project and tool config.")
if hasattr(baseobject, "Shape") and not isPanel:
#bb = baseobject.Shape.BoundBox
env = PathUtils.getEnvelope(partshape=baseobject.Shape, subshape=None, depthparams=self.depthparams)
try:
(pp, sim) = self._buildPathArea(obj, env, start=obj.StartPoint,getsim=getsim)
(pp, sim) = self._buildPathArea(obj, env, start=obj.StartPoint, getsim=getsim)
commandlist.extend(pp.Commands)
except Exception as e:
FreeCAD.Console.PrintError(e)
@@ -268,9 +273,9 @@ class ObjectContour:
# Let's finish by rapid to clearance...just for safety
commandlist.append(Path.Command("G0", {"Z": obj.ClearanceHeight.Value}))
PathLog.track()
path = Path.Path(commandlist)
obj.Path = path
#obj.ViewObject.Visibility = True
return sim
@@ -371,7 +376,6 @@ class CommandPathContour:
FreeCADGui.doCommand('obj.ToolController = PathScripts.PathUtils.findToolController(obj)')
FreeCAD.ActiveDocument.commitTransaction()
#FreeCAD.ActiveDocument.recompute()
FreeCADGui.doCommand('obj.ViewObject.startEditing()')
@@ -379,7 +383,6 @@ class TaskPanel:
def __init__(self, obj, deleteOnReject):
FreeCAD.ActiveDocument.openTransaction(translate("Path_Contour", "Contour Operation"))
self.form = FreeCADGui.PySideUic.loadUi(":/panels/ContourEdit.ui")
# self.form = FreeCADGui.PySideUic.loadUi(FreeCAD.getHomePath() + "Mod/Path/ContourEdit.ui")
self.deleteOnReject = deleteOnReject
self.isDirty = True
@@ -402,7 +405,7 @@ class TaskPanel:
FreeCAD.ActiveDocument.commitTransaction()
FreeCAD.ActiveDocument.recompute()
def clicked(self,button):
def clicked(self, button):
if button == QtGui.QDialogButtonBox.Apply:
self.getFields()
FreeCAD.ActiveDocument.recompute()

View File

@@ -39,10 +39,10 @@ if True:
else:
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
#FreeCADGui = None
if FreeCAD.GuiUp:
import FreeCADGui
# Qt tanslation handling
def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
@@ -95,8 +95,12 @@ class ObjectFace:
obj.addProperty("App::PropertyBool", "UseStartPoint", "Start Point", QtCore.QT_TRANSLATE_NOOP("App::Property", "make True, if specifying a Start Point"))
# Debug Parameters
# obj.addProperty("App::PropertyString", "AreaParams", "Debug", QtCore.QT_TRANSLATE_NOOP("App::Property", "parameters used by PathArea"))
# obj.setEditorMode('AreaParams', 2) # hide
obj.addProperty("App::PropertyString", "AreaParams", "Path")
obj.setEditorMode('AreaParams', 2) # hide
obj.addProperty("App::PropertyString", "PathParams", "Path")
obj.setEditorMode('PathParams', 2) # hide
obj.addProperty("Part::PropertyPartShape", "removalshape", "Path")
obj.setEditorMode('removalshape', 2) # hide
if FreeCAD.GuiUp:
_ViewProviderFace(obj.ViewObject)
@@ -108,6 +112,8 @@ class ObjectFace:
if prop == "StepOver":
if obj.StepOver == 0:
obj.StepOver = 1
if prop in ['AreaParams', 'PathParams', 'removalshape']:
obj.setEditorMode(prop, 2)
def __getstate__(self):
return None
@@ -115,7 +121,6 @@ class ObjectFace:
def __setstate__(self, state):
return None
def setDepths(self, obj):
PathLog.track()
parentJob = PathUtils.findParentJob(obj)
@@ -139,8 +144,8 @@ class ObjectFace:
if len(baselist) == 0: # When adding the first base object, guess at heights
subshape = [ss.Shape.getElement(sub)]
d = PathUtils.guessDepths(ss.Shape, subshape)
obj.ClearanceHeight =d.clearance_height
obj.SafeHeight = d.safe_height +1
obj.ClearanceHeight = d.clearance_height
obj.SafeHeight = d.safe_height + 1
obj.StartDepth = d.safe_height
obj.FinalDepth = d.final_depth
obj.StepDown = obj.StartDepth.Value-obj.FinalDepth.Value
@@ -155,7 +160,6 @@ class ObjectFace:
baselist.append(item)
PathLog.debug('baselist: {}'.format(baselist))
obj.Base = baselist
#self.execute(obj)
def getStock(self, obj):
"""find and return a stock object from hosting project if any"""
@@ -197,23 +201,34 @@ class ObjectFace:
heights = [i for i in self.depthparams]
boundary.setParams(**pocketparams)
#obj.AreaParams = str(boundary.getParams())
#PathLog.track('areaparams: {}'.format(obj.AreaParams))
PathLog.track('height: {}'.format(heights))
obj.AreaParams = str(boundary.getParams())
sections = boundary.makeSections(mode=0, project=False, heights=heights)
shapelist = [sec.getShape() for sec in sections]
params = {'shapes': shapelist,
'feedrate': self.horizFeed,
params = {'feedrate': self.horizFeed,
'feedrate_v': self.vertFeed,
'verbose': True,
'resume_height': obj.StepDown,
'retraction': obj.ClearanceHeight.Value}
PathLog.debug("Generating Path with params: {}".format(params))
pp = Path.fromShapes(**params)
pp = []
return pp
if obj.UseStartPoint and obj.StartPoint is not None:
params['start'] = obj.StartPoint
# store the params for debugging. Don't need the shape.
obj.PathParams = str(params)
PathLog.debug("Generating Path with params: {}".format(params))
for sec in sections:
shape = sec.getShape()
respath = Path.fromShapes(shape, **params)
# Insert any entry code to the layer
# append the layer path
pp.extend(respath.Commands)
respath.Commands = pp
return respath
def execute(self, obj):
PathLog.track()
@@ -229,7 +244,7 @@ class ObjectFace:
self.depthparams = depth_params(
clearance_height=obj.ClearanceHeight.Value,
safe_height=obj.SafeHeight.Value,
start_depth=obj.StartDepth.Value,
start_depth=obj.SafeHeight.Value,
step_down=obj.StepDown,
z_finish_step=obj.FinishDepth.Value,
final_depth=obj.FinalDepth.Value,
@@ -281,9 +296,6 @@ class ObjectFace:
planeshape = baseobject.Shape
PathLog.info("Working on a shape {}".format(baseobject.Name))
# Let's start by rapid to clearance...just for safety
commandlist.append(Path.Command("G0", {"Z": obj.ClearanceHeight.Value}))
# if user wants the boundbox, calculate that
PathLog.info("Boundary Shape: {}".format(obj.BoundaryShape))
bb = planeshape.BoundBox
@@ -293,6 +305,9 @@ class ObjectFace:
else:
env = PathUtils.getEnvelope(partshape=planeshape, depthparams=self.depthparams)
# save the envelope for reference
obj.removalshape = env
try:
commandlist.extend(self._buildPathArea(obj, env).Commands)
except Exception as e:
@@ -354,8 +369,6 @@ class CommandPathMillFace:
return False
def Activated(self):
#ztop = 10.0
# if everything is ok, execute and register the transaction in the undo/redo stack
FreeCAD.ActiveDocument.openTransaction(translate("PathFace", "Create Face"))
FreeCADGui.addModule("PathScripts.PathMillFace")
@@ -368,7 +381,7 @@ class CommandPathMillFace:
FreeCADGui.doCommand('obj.Active = True')
FreeCADGui.doCommand('obj.StepOver = 50')
#FreeCADGui.doCommand('obj.StepDown = 1.0')
FreeCADGui.doCommand('obj.StepDown = 1.0')
FreeCADGui.doCommand('obj.ZigZagAngle = 45.0')
FreeCAD.ActiveDocument.commitTransaction()
@@ -392,6 +405,7 @@ class _CommandSetFaceStartPoint:
def Activated(self):
FreeCADGui.Snapper.getPoint(callback=self.setpoint)
class TaskPanel:
def __init__(self, obj, deleteOnReject):
FreeCAD.ActiveDocument.openTransaction(translate("Path_MillFace", "Mill Facing Operation"))
@@ -420,7 +434,7 @@ class TaskPanel:
FreeCAD.ActiveDocument.commitTransaction()
FreeCAD.ActiveDocument.recompute()
def clicked(self,button):
def clicked(self, button):
if button == QtGui.QDialogButtonBox.Apply:
self.getFields()
FreeCAD.ActiveDocument.recompute()

View File

@@ -77,21 +77,27 @@ class ObjectPocket:
obj.addProperty("App::PropertyFloat", "ZigZagAngle", "Pocket", QtCore.QT_TRANSLATE_NOOP("App::Property", "Angle of the zigzag pattern"))
obj.addProperty("App::PropertyEnumeration", "OffsetPattern", "Face", QtCore.QT_TRANSLATE_NOOP("App::Property", "clearing pattern to use"))
obj.OffsetPattern = ['ZigZag', 'Offset', 'Spiral', 'ZigZagOffset', 'Line', 'Grid', 'Triangle']
obj.addProperty("App::PropertyBool", "MinTravel", "Pocket", QtCore.QT_TRANSLATE_NOOP("App::Property", "Use 3D Sorting of Path"))
# Start Point Properties
obj.addProperty("App::PropertyVector", "StartPoint", "Start Point", QtCore.QT_TRANSLATE_NOOP("App::Property", "The start point of this path"))
obj.addProperty("App::PropertyBool", "UseStartPoint", "Start Point", QtCore.QT_TRANSLATE_NOOP("App::Property", "make True, if specifying a Start Point"))
# Debug Parameters
obj.addProperty("App::PropertyString", "AreaParams", "Debug", QtCore.QT_TRANSLATE_NOOP("App::Property", "parameters used by PathArea"))
obj.addProperty("App::PropertyString", "AreaParams", "Path")
obj.setEditorMode('AreaParams', 2) # hide
obj.addProperty("App::PropertyString", "PathParams", "Path")
obj.setEditorMode('PathParams', 2) # hide
obj.addProperty("Part::PropertyPartShape", "removalshape", "Path")
obj.setEditorMode('removalshape', 2) # hide
if FreeCAD.GuiUp:
ViewProviderPocket(obj.ViewObject)
obj.Proxy = self
def onChanged(self, obj, prop):
pass
if prop in ['AreaParams', 'PathParams', 'removalshape']:
obj.setEditorMode(prop, 2)
def __getstate__(self):
return None
@@ -211,6 +217,17 @@ class ObjectPocket:
'resume_height': obj.StepDown.Value,
'retraction': obj.ClearanceHeight.Value}
if obj.UseStartPoint and obj.StartPoint is not None:
params['start'] = obj.StartPoint
# if MinTravel is turned on, set path sorting to 3DSort
# 3DSort shouldn't be used without a valid start point. Can cause
# tool crash without it.
if obj.MinTravel:
params['sort_mode'] = 2
obj.PathParams = str({key: value for key, value in params.items() if key != 'shapes'})
pp = Path.fromShapes(**params)
PathLog.debug("Generating Path with params: {}".format(params))
PathLog.debug(pp)
@@ -218,12 +235,10 @@ class ObjectPocket:
simobj = None
if getsim:
pocketparams['Thicken'] = True
pocketparams['ToolRadius']= self.radius - self.radius *.005
pocketparams['ToolRadius'] = self.radius - self.radius * .005
pocketparams['Stepdown'] = -1
pocket.setParams(**pocketparams)
#pocket.makeSections(mode=0, project=False, heights=heights)
simobj = pocket.getShape().extrude(FreeCAD.Vector(0,0,obj.StepDown.Value))
#removalshape = FreeCAD.ActiveDocument.addObject("Part::Feature", "simshape")
simobj = pocket.getShape().extrude(FreeCAD.Vector(0, 0, obj.StepDown.Value))
return pp, simobj
@@ -276,20 +291,15 @@ class ObjectPocket:
for sub in b[1]:
if "Face" in sub:
shape = Part.makeCompound([getattr(b[0].Shape, sub)])
#shape = getattr(b[0].Shape, sub)
else:
edges = [getattr(b[0].Shape, sub) for sub in b[1]]
shape = Part.makeFace(edges, 'Part::FaceMakerSimple')
env = PathUtils.getEnvelope(baseobject.Shape, subshape=shape, depthparams=self.depthparams)
removal = env.cut(baseobject.Shape)
if PathLog.getLevel(PathLog.thisModule()) == PathLog.Level.DEBUG:
removalshape=FreeCAD.ActiveDocument.addObject("Part::Feature","removalshape")
removalshape.Shape = removal
obj.removalshape = env.cut(baseobject.Shape)
try:
(pp, sim) = self._buildPathArea(obj, removal, getsim=getsim)
(pp, sim) = self._buildPathArea(obj, obj.removalshape, getsim=getsim)
if sim is not None:
simlist.append(sim)
commandlist.extend(pp.Commands)
@@ -300,17 +310,13 @@ class ObjectPocket:
PathLog.debug("processing the whole job base object")
env = PathUtils.getEnvelope(baseobject.Shape, subshape=None, depthparams=self.depthparams)
removal = env.cut(baseobject.Shape)
if PathLog.getLevel(PathLog.thisModule()) == PathLog.Level.DEBUG:
removalshape=FreeCAD.ActiveDocument.addObject("Part::Feature","removalshape")
removalshape.Shape = removal
obj.removalshape = env.cut(baseobject.Shape)
try:
(pp, sim) = self._buildPathArea(obj, removal, getsim=getsim)
(pp, sim) = self._buildPathArea(obj, obj.removalshape, getsim=getsim)
commandlist.extend(pp.Commands)
if sim is not None:
simlist.append(sim)
#commandlist.extend(self._buildPathArea(obj, env.cut(baseobject.Shape)).Commands)
except Exception as e:
FreeCAD.Console.PrintError(e)
FreeCAD.Console.PrintError("Something unexpected happened. Unable to generate a pocket path. Check project and tool config.")
@@ -325,15 +331,16 @@ class ObjectPocket:
PathLog.debug(simlist)
simshape = None
if len(simlist) > 1:
simshape=simlist[0].fuse(simlist[1:])
simshape = simlist[0].fuse(simlist[1:])
elif len(simlist) == 1:
simshape = simlist[0]
if simshape is not None and PathLog.getLevel(PathLog.thisModule()) == PathLog.Level.DEBUG:
sim=FreeCAD.ActiveDocument.addObject("Part::Feature","simshape")
sim = FreeCAD.ActiveDocument.addObject("Part::Feature", "simshape")
sim.Shape = simshape
return simshape
class _CommandSetPocketStartPoint:
def GetResources(self):
return {'Pixmap': 'Path-StartPoint',
@@ -373,7 +380,6 @@ class ViewProviderPocket:
self.deleteOnReject = False
return True
def getIcon(self):
return ":/icons/Path-Pocket.svg"
@@ -411,7 +417,6 @@ class CommandPathPocket:
FreeCADGui.doCommand('obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", "Pocket")')
FreeCADGui.doCommand('PathScripts.PathPocket.ObjectPocket(obj)')
FreeCADGui.doCommand('obj.Active = True')
#FreeCADGui.doCommand('PathScripts.PathPocket.ViewProviderPocket(obj.ViewObject)')
FreeCADGui.doCommand('obj.ViewObject.Proxy.deleteOnReject = True')
FreeCADGui.doCommand('from PathScripts import PathUtils')
FreeCADGui.doCommand('obj.StepOver = 100')
@@ -456,7 +461,7 @@ class TaskPanel:
FreeCAD.ActiveDocument.commitTransaction()
FreeCAD.ActiveDocument.recompute()
def clicked(self,button):
def clicked(self, button):
if button == QtGui.QDialogButtonBox.Apply:
self.getFields()
self.obj.Proxy.execute(self.obj)

View File

@@ -86,8 +86,12 @@ class ObjectProfile:
obj.addProperty("App::PropertyBool", "processCircles", "Profile", QtCore.QT_TRANSLATE_NOOP("App::Property", "Profile round holes"))
# Debug Parameters
obj.addProperty("App::PropertyString", "AreaParams", "Debug", QtCore.QT_TRANSLATE_NOOP("App::Property", "parameters used by PathArea"))
obj.addProperty("App::PropertyString", "AreaParams", "Path")
obj.setEditorMode('AreaParams', 2) # hide
obj.addProperty("App::PropertyString", "PathParams", "Path")
obj.setEditorMode('PathParams', 2) # hide
obj.addProperty("Part::PropertyPartShape", "removalshape", "Path")
obj.setEditorMode('removalshape', 2) # hide
if FreeCAD.GuiUp:
_ViewProviderProfile(obj.ViewObject)
@@ -106,6 +110,8 @@ class ObjectProfile:
obj.setEditorMode('Side', 2)
else:
obj.setEditorMode('Side', 0)
if prop in ['AreaParams', 'PathParams', 'removalshape']:
obj.setEditorMode(prop, 2)
def addprofilebase(self, obj, ss, sub=""):
baselist = obj.Base
@@ -188,7 +194,7 @@ class ObjectProfile:
'resume_height': obj.StepDown.Value,
'retraction': obj.ClearanceHeight.Value}
#Reverse the direction for holes
# Reverse the direction for holes
if isHole:
direction = "CW" if obj.Direction == "CCW" else "CCW"
else:
@@ -203,16 +209,19 @@ class ObjectProfile:
params['start'] = obj.StartPoint
pp = Path.fromShapes(**params)
obj.PathParams = str({key: value for key, value in params.items() if key != 'shapes'})
PathLog.debug("Generating Path with params: {}".format(params))
PathLog.debug(pp)
simobj = None
if getsim:
profileparams['Thicken'] = True #{'Fill':0, 'Coplanar':0, 'Project':True, 'SectionMode':2, 'Thicken':True}
profileparams['ToolRadius']= self.radius - self.radius *.005
profileparams['Thicken'] = True
profileparams['ToolRadius'] = self.radius - self.radius * .005
profile.setParams(**profileparams)
sec = profile.makeSections(mode=0, project=False, heights=heights)[-1].getShape()
simobj = sec.extrude(FreeCAD.Vector(0,0,baseobject.BoundBox.ZMax))
simobj = sec.extrude(FreeCAD.Vector(0, 0, baseobject.BoundBox.ZMax))
return pp, simobj
@@ -265,7 +274,7 @@ class ObjectProfile:
if baseobject is None:
return
if obj.Base: # The user has selected subobjects from the base. Process each.
if obj.Base: # The user has selected subobjects from the base. Process each.
holes = []
faces = []
for b in obj.Base:
@@ -276,7 +285,7 @@ class ObjectProfile:
if numpy.isclose(abs(shape.normalAt(0, 0).z), 1): # horizontal face
holes += shape.Wires[1:]
else:
FreeCAD.Console.PrintWarning ("found a base object which is not a face. Can't continue.")
FreeCAD.Console.PrintWarning("found a base object which is not a face. Can't continue.")
return
for wire in holes:
@@ -428,7 +437,6 @@ class CommandPathProfile:
FreeCADGui.doCommand('obj.UseComp = True')
FreeCADGui.doCommand('obj.processHoles = False')
FreeCADGui.doCommand('obj.processPerimeter = True')
#FreeCADGui.doCommand('PathScripts.PathProfile._ViewProviderProfile(obj.ViewObject)')
FreeCADGui.doCommand('obj.ViewObject.Proxy.deleteOnReject = True')
FreeCADGui.doCommand('PathScripts.PathUtils.addToJob(obj)')
FreeCADGui.doCommand('obj.ToolController = PathScripts.PathUtils.findToolController(obj)')
@@ -465,7 +473,7 @@ class TaskPanel:
FreeCAD.ActiveDocument.commitTransaction()
FreeCAD.ActiveDocument.recompute()
def clicked(self,button):
def clicked(self, button):
if button == QtGui.QDialogButtonBox.Apply:
self.getFields()
self.obj.Proxy.execute(self.obj)

View File

@@ -44,6 +44,7 @@ if FreeCAD.GuiUp:
import FreeCADGui
from PySide import QtCore, QtGui
# Qt tanslation handling
def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
@@ -89,8 +90,12 @@ class ObjectProfile:
obj.addProperty("App::PropertyDistance", "OffsetExtra", "Profile", QtCore.QT_TRANSLATE_NOOP("App::Property", "Extra value to stay away from final profile- good for roughing toolpath"))
# Debug Parameters
obj.addProperty("App::PropertyString", "AreaParams", "Debug", QtCore.QT_TRANSLATE_NOOP("App::Property", "parameters used by PathArea"))
obj.addProperty("App::PropertyString", "AreaParams", "Path")
obj.setEditorMode('AreaParams', 2) # hide
obj.addProperty("App::PropertyString", "PathParams", "Path")
obj.setEditorMode('PathParams', 2) # hide
obj.addProperty("Part::PropertyPartShape", "removalshape", "Path")
obj.setEditorMode('removalshape', 2) # hide
if FreeCAD.GuiUp:
_ViewProviderProfile(obj.ViewObject)
@@ -109,6 +114,8 @@ class ObjectProfile:
obj.setEditorMode('Side', 2)
else:
obj.setEditorMode('Side', 0)
if prop in ['AreaParams', 'PathParams', 'removalshape']:
obj.setEditorMode(prop, 2)
def addprofilebase(self, obj, ss, sub=""):
baselist = obj.Base
@@ -142,8 +149,6 @@ class ObjectProfile:
else:
baselist.append(item)
obj.Base = baselist
#self.execute(obj)
@waiting_effects
def _buildPathArea(self, obj, baseobject, start=None, getsim=False):
@@ -163,9 +168,7 @@ class ObjectProfile:
else:
profileparams['Offset'] = self.radius+obj.OffsetExtra.Value
profile.setParams(**profileparams)
# PathLog.debug("About to profile with params: {}".format(profileparams))
obj.AreaParams = str(profile.getParams())
PathLog.debug("About to profile with params: {}".format(profile.getParams()))
@@ -191,21 +194,21 @@ class ObjectProfile:
pp = Path.fromShapes(**params)
PathLog.debug("Generating Path with params: {}".format(params))
PathLog.debug(pp)
# store the params for debugging. Don't need the shape.
obj.PathParams = str({key: value for key, value in params.items() if key != 'shapes'})
simobj = None
if getsim:
profileparams['Thicken'] = True #{'Fill':0, 'Coplanar':0, 'Project':True, 'SectionMode':2, 'Thicken':True}
profileparams['ToolRadius']= self.radius - self.radius *.005
profileparams['Thicken'] = True
profileparams['ToolRadius'] = self.radius - self.radius * .005
profile.setParams(**profileparams)
sec = profile.makeSections(mode=0, project=False, heights=heights)[-1].getShape()
simobj = sec.extrude(FreeCAD.Vector(0,0,baseobject.BoundBox.ZMax))
simobj = sec.extrude(FreeCAD.Vector(0, 0, baseobject.BoundBox.ZMax))
return pp, simobj
def execute(self, obj, getsim=False):
# import Part # math #DraftGeomUtils
commandlist = []
sim = None
@@ -380,7 +383,6 @@ class CommandPathProfileEdges:
FreeCADGui.addModule("PathScripts.PathProfile")
FreeCADGui.doCommand('obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", "Edge Profile")')
FreeCADGui.doCommand('PathScripts.PathProfileEdges.ObjectProfile(obj)')
#FreeCADGui.doCommand('PathScripts.PathProfileEdges._ViewProviderProfile(obj.ViewObject)')
FreeCADGui.doCommand('obj.ViewObject.Proxy.deleteOnReject = True')
FreeCADGui.doCommand('obj.Active = True')
@@ -408,14 +410,11 @@ class TaskPanel:
def __init__(self, obj, deleteOnReject):
FreeCAD.ActiveDocument.openTransaction(translate("Path_ProfileEdges", "ProfileEdges Operation"))
self.form = FreeCADGui.PySideUic.loadUi(":/panels/ProfileEdgesEdit.ui")
# self.form = FreeCADGui.PySideUic.loadUi(FreeCAD.getHomePath() + "Mod/Path/ProfileEdgesEdit.ui")
self.deleteOnReject = deleteOnReject
self.obj = obj
self.isDirty = True
def accept(self):
#self.getFields()
FreeCADGui.Control.closeDialog()
FreeCADGui.ActiveDocument.resetEdit()
FreeCAD.ActiveDocument.commitTransaction()
@@ -434,7 +433,7 @@ class TaskPanel:
FreeCAD.ActiveDocument.commitTransaction()
FreeCAD.ActiveDocument.recompute()
def clicked(self,button):
def clicked(self, button):
if button == QtGui.QDialogButtonBox.Apply:
self.getFields()
self.obj.Proxy.execute(self.obj)

View File

@@ -1,142 +1,338 @@
# -*- coding: utf-8 -*-
#***************************************************************************
#* *
#* Copyright (c) 2015 Dan Falck <ddfalck@gmail.com> *
#* *
#* This program is free software; you can redistribute it and/or modify *
#* it under the terms of the GNU Lesser General Public License (LGPL) *
#* as published by the Free Software Foundation; either version 2 of *
#* the License, or (at your option) any later version. *
#* for detail see the LICENCE text file. *
#* *
#* This program is distributed in the hope that it will be useful, *
#* but WITHOUT ANY WARRANTY; without even the implied warranty of *
#* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
#* GNU Library General Public License for more details. *
#* *
#* You should have received a copy of the GNU Library General Public *
#* License along with this program; if not, write to the Free Software *
#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
#* USA *
#* *
#***************************************************************************
# ***************************************************************************
# * Copyright (c) 2015 Dan Falck <ddfalck@gmail.com> *
# * *
# * This file is part of the FreeCAD CAx development system. *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * FreeCAD is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Lesser General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with FreeCAD; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************/
from __future__ import print_function
TOOLTIP=''' example post for Centroid CNC mill'''
TOOLTIP='''
This is a postprocessor file for the Path workbench. It is used to
take a pseudo-gcode fragment outputted by a Path object, and output
real GCode suitable for a centroid 3 axis mill. This postprocessor, once placed
in the appropriate PathScripts folder, can be used directly from inside
FreeCAD, via the GUI importer or via python scripts with:
import centroid_post
centroid_post.export(object,"/path/to/file.ncc","")
'''
TOOLTIP_ARGS='''
Arguments for centroid:
--header,--no-header ... output headers (--header)
--comments,--no-comments ... output comments (--comments)
--line-numbers,--no-line-numbers ... prefix with line numbers (--no-lin-numbers)
--show-editor, --no-show-editor ... pop up editor before writing output(--show-editor)
--feed-precision=1 ... number of digits of precision for feed rate. Default=1
--axis-precision=4 ... number of digits of precision for axis moves. Default=4
'''
import FreeCAD
from FreeCAD import Units
import datetime
now = datetime.datetime.now()
import PathScripts
from PathScripts import PostUtils
#from PathScripts import PathUtils
now = datetime.datetime.now()
#***************************************************************************
# user editable stuff here
# These globals set common customization preferences
OUTPUT_COMMENTS = True
OUTPUT_HEADER = True
OUTPUT_LINE_NUMBERS = False
if FreeCAD.GuiUp:
SHOW_EDITOR = True
else:
SHOW_EDITOR = False
MODAL = False # if true commands are suppressed if the same as previous line.
UNITS = "G20" #old style inch units for this shop
MACHINE_NAME = "BigMill"
COMMAND_SPACE = " "
LINENR = 100 # line number starting value
# These globals will be reflected in the Machine configuration of the project
UNITS = "G20" # G21 for metric, G20 for us standard
UNIT_FORMAT = 'mm/min'
MACHINE_NAME = "Centroid"
CORNER_MIN = {'x':-609.6, 'y':-152.4, 'z':0 } #use metric for internal units
CORNER_MAX = {'x':609.6, 'y':152.4, 'z':304.8 } #use metric for internal units
SHOW_EDITOR = True
MODAL = True
COMMENT= ';' #centroid control comment symbol
HEADER = ""
HEADER += ";Exported by FreeCAD\n"
HEADER += ";Post Processor: " + __name__ +"\n"
HEADER += ";CAM file: %s\n"
HEADER += ";Output Time:"+str(now)+"\n"
TOOLRETURN = '''M5 M25
G49 H0\n''' #spindle off,height offset canceled,spindle retracted (M25 is a centroid command to retract spindle)
ZAXISRETURN = '''G91 G28 X0 Z0
G90\n'''
SAFETYBLOCK = 'G90 G80 G40 G49\n'
AXIS_DECIMALS = 4
FEED_DECIMALS = 1
AXIS_PRECISION=4
FEED_PRECISION=1
SPINDLE_DECIMALS = 0
FOOTER = 'M99'+'\n'
COMMENT = ";"
# don't edit with the stuff below the next line unless you know what you're doing :)
#***************************************************************************
HEADER = '''
;Exported by FreeCAD
;Post Processor: {}
;CAM file: {}
;Output Time: {}
'''.format(__name__, FreeCAD.ActiveDocument.FileName, str(now))
# Preamble text will appear at the beginning of the GCODE output file.
PREAMBLE = '''G53 G00 G17
'''
# Postamble text will appear following the last operation.
POSTAMBLE = '''M99
'''
TOOLRETURN = '''M5 M25
G49 H0
''' #spindle off,height offset canceled,spindle retracted (M25 is a centroid command to retract spindle)
ZAXISRETURN = '''G91 G28 X0 Z0
G90
'''
SAFETYBLOCK = '''G90 G80 G40 G49
'''
# Pre operation text will be inserted before every operation
PRE_OPERATION = ''''''
# Post operation text will be inserted after every operation
POST_OPERATION = ''''''
# Tool Change commands will be inserted before a tool change
TOOL_CHANGE = ''''''
# to distinguish python built-in open function from the one declared below
if open.__module__ == '__builtin__':
pythonopen = open
def export(selection,filename,argstring):
params = ['X','Y','Z','A','B','I','J','F','H','S','T','Q','R','L'] #Using XY plane most of the time so skipping K
for obj in selection:
if not hasattr(obj,"Path"):
print("the object " + obj.Name + " is not a path. Please select only path and Compounds.")
return
myMachine = None
for pathobj in selection:
if hasattr(pathobj,"MachineName"):
myMachine = pathobj.MachineName
if hasattr(pathobj, "MachineUnits"):
if pathobj.MachineUnits == "Metric":
UNITS = "G21"
else:
UNITS = "G20"
if myMachine is None:
print("No machine found in this selection")
def processArguments(argstring):
global OUTPUT_HEADER
global OUTPUT_COMMENTS
global OUTPUT_LINE_NUMBERS
global SHOW_EDITOR
global AXIS_PRECISION
global FEED_PRECISION
gcode =''
gcode+= HEADER % (FreeCAD.ActiveDocument.FileName)
gcode+= SAFETYBLOCK
gcode+= UNITS+'\n'
for arg in argstring.split():
if arg == '--header':
OUTPUT_HEADER = True
elif arg == '--no-header':
OUTPUT_HEADER = False
elif arg == '--comments':
OUTPUT_COMMENTS = True
elif arg == '--no-comments':
OUTPUT_COMMENTS = False
elif arg == '--line-numbers':
OUTPUT_LINE_NUMBERS = True
elif arg == '--no-line-numbers':
OUTPUT_LINE_NUMBERS = False
elif arg == '--show-editor':
SHOW_EDITOR = True
elif arg == '--no-show-editor':
SHOW_EDITOR = False
elif arg.split('=')[0] == '--axis-precision':
AXIS_PRECISION = arg.split('=')[1]
elif arg.split('=')[0] == '--feed-precision':
FEED_PRECISION = arg.split('=')[1]
def export(objectslist, filename, argstring):
processArguments(argstring)
for i in objectslist:
print (i.Name)
global UNITS
global UNIT_FORMAT
# ISJOB = (len(objectslist) == 1) and isinstance(objectslist[0].Proxy, PathScripts.PathJob.ObjectPathJob)
# print("isjob: {} {}".format(ISJOB, len(objectslist)))
# if len(objectslist) > 1:
# for obj in objectslist:
# if not hasattr(obj, "Path"):
# print("the object " + obj.Name + " is not a path. Please select only path and Compounds.")
# return
print("postprocessing...")
gcode = ""
# write header
if OUTPUT_HEADER:
gcode += HEADER
gcode += SAFETYBLOCK
# Write the preamble
if OUTPUT_COMMENTS:
for item in objectslist:
if isinstance (item.Proxy, PathScripts.PathToolController.ToolController):
gcode += ";T{}={}\n".format(item.ToolNumber, item.Name)
gcode += linenumber() + ";begin preamble\n"
for line in PREAMBLE.splitlines(True):
gcode += linenumber() + line
gcode += linenumber() + UNITS + "\n"
for obj in objectslist:
#skip postprocessing tools
# if isinstance (obj.Proxy, PathScripts.PathToolController.ToolController):
# continue
# do the pre_op
if OUTPUT_COMMENTS:
gcode += linenumber() + ";begin operation\n"
for line in PRE_OPERATION.splitlines(True):
gcode += linenumber() + line
gcode += parse(obj)
# do the post_op
if OUTPUT_COMMENTS:
gcode += linenumber() + ";end operation: %s\n" % obj.Label
for line in POST_OPERATION.splitlines(True):
gcode += linenumber() + line
# do the post_amble
if OUTPUT_COMMENTS:
gcode += ";begin postamble\n"
for line in TOOLRETURN.splitlines(True):
gcode += linenumber() + line
for line in SAFETYBLOCK.splitlines(True):
gcode += linenumber() + line
for line in POSTAMBLE.splitlines(True):
gcode += linenumber() + line
if SHOW_EDITOR:
dia = PostUtils.GCodeEditorDialog()
dia.editor.setText(gcode)
result = dia.exec_()
if result:
final = dia.editor.toPlainText()
else:
final = gcode
else:
final = gcode
print("done postprocessing.")
if not filename == '-':
gfile = pythonopen(filename, "wb")
gfile.write(final)
gfile.close()
return final
def linenumber():
global LINENR
if OUTPUT_LINE_NUMBERS is True:
LINENR += 10
return "N" + str(LINENR) + " "
return ""
def parse(pathobj):
global AXIS_PRECISION
global FEED_PRECISION
out = ""
lastcommand = None
gcode+= COMMENT+ selection[0].Description +'\n'
axis_precision_string = '.' + str(AXIS_PRECISION) +'f'
feed_precision_string = '.' + str(FEED_PRECISION) +'f'
# params = ['X','Y','Z','A','B','I','J','K','F','S'] #This list control
# the order of parameters
# centroid doesn't want K properties on XY plane Arcs need work.
params = ['X', 'Y', 'Z', 'A', 'B', 'I', 'J', 'F', 'S', 'T', 'Q', 'R', 'L', 'H']
gobjects = []
for g in selection[0].Group:
gobjects.append(g)
if hasattr(pathobj, "Group"): # We have a compound or project.
# if OUTPUT_COMMENTS:
# out += linenumber() + "(compound: " + pathobj.Label + ")\n"
for p in pathobj.Group:
out += parse(p)
return out
else: # parsing simple path
for obj in gobjects:
for c in obj.Path.Commands:
outstring = []
command = c.Name
# groups might contain non-path things like stock.
if not hasattr(pathobj, "Path"):
return out
# if OUTPUT_COMMENTS:
# out += linenumber() + "(" + pathobj.Label + ")\n"
for c in pathobj.Path.Commands:
commandlist = [] #list of elements in the command, code and params.
command = c.Name #command M or G code or comment string
if command[0]=='(':
command = PostUtils.fcoms(command, COMMENT)
outstring.append(command)
if MODAL == True:
commandlist.append(command)
# if modal: only print the command if it is not the same as the
# last one
if MODAL is True:
if command == lastcommand:
outstring.pop(0)
if c.Parameters >= 1:
for param in params:
if param in c.Parameters:
if param == 'F':
outstring.append(param + PostUtils.fmt(c.Parameters['F'], FEED_DECIMALS,UNITS))
elif param == 'H':
outstring.append(param + str(int(c.Parameters['H'])))
elif param == 'S':
outstring.append(param + PostUtils.fmt(c.Parameters['S'], SPINDLE_DECIMALS,'G21')) #rpm is unitless-therefore I had to 'fake it out' by using metric units which don't get converted from entered value
elif param == 'T':
outstring.append(param + str(int(c.Parameters['T'])))
else:
outstring.append(param + PostUtils.fmt(c.Parameters[param],AXIS_DECIMALS,UNITS))
outstr = str(outstring)
commandlist.pop(0)
# Now add the remaining parameters in order
for param in params:
if param in c.Parameters:
if param == 'F':
if c.Name not in ["G0", "G00"]: #centroid doesn't use rapid speeds
speed = Units.Quantity(c.Parameters['F'], FreeCAD.Units.Velocity)
commandlist.append(
param + format(float(speed.getValueAs(UNIT_FORMAT)), feed_precision_string) )
elif param == 'H':
commandlist.append(param + str(int(c.Parameters['H'])))
elif param == 'S':
commandlist.append(param + PostUtils.fmt(c.Parameters['S'], SPINDLE_DECIMALS, "G21"))
elif param == 'T':
commandlist.append(param + str(int(c.Parameters['T'])))
else:
commandlist.append(
param + format(c.Parameters[param], axis_precision_string))
outstr = str(commandlist)
outstr =outstr.replace('[','')
outstr =outstr.replace(']','')
outstr =outstr.replace("'",'')
outstr =outstr.replace(",",'')
gcode+= outstr + '\n'
lastcommand = c.Name
gcode+= TOOLRETURN
gcode+= SAFETYBLOCK
gcode+= FOOTER
if SHOW_EDITOR:
PostUtils.editor(gcode)
gfile = pythonopen(filename,"wb")
gfile.write(gcode)
gfile.close()
#out += outstr + '\n'
# store the latest command
lastcommand = command
# Check for Tool Change:
if command == 'M6':
# if OUTPUT_COMMENTS:
# out += linenumber() + "(begin toolchange)\n"
for line in TOOL_CHANGE.splitlines(True):
out += linenumber() + line
# if command == "message":
# if OUTPUT_COMMENTS is False:
# out = []
# else:
# commandlist.pop(0) # remove the command
# prepend a line number and append a newline
if len(commandlist) >= 1:
if OUTPUT_LINE_NUMBERS:
commandlist.insert(0, (linenumber()))
# append the line to the final output
for w in commandlist:
out += w + COMMAND_SPACE
out = out.strip() + "\n"
return out
print(__name__ + " gcode postprocessor loaded.")

View File

@@ -34,6 +34,14 @@ from GCode.
import os
import Path
import FreeCAD
import PathScripts.PathLog as PathLog
if True:
PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
PathLog.trackModule(PathLog.thisModule())
else:
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
# to distinguish python built-in open function from the one declared below
if open.__module__ == '__builtin__':
@@ -41,6 +49,7 @@ if open.__module__ == '__builtin__':
def open(filename):
PathLog.track(filename)
"called when freecad opens a file."
docname = os.path.splitext(os.path.basename(filename))[0]
doc = FreeCAD.newDocument(docname)
@@ -49,6 +58,7 @@ def open(filename):
def insert(filename, docname):
"called when freecad imports a file"
PathLog.track(filename)
gfile = pythonopen(filename)
gcode = gfile.read()
gfile.close()
@@ -61,12 +71,14 @@ def insert(filename, docname):
def parse(inputstring):
"parse(inputstring): returns a parsed output string"
print("preprocessing...")
print(inputstring)
PathLog.track(inputstring)
# split the input by line
lines = inputstring.split("\n")
output = ""
lastcommand = None
print (lines)
for l in lines:
# remove any leftover trailing and preceding spaces
l = l.strip()
@@ -75,8 +87,14 @@ def parse(inputstring):
continue
if l[0].upper() in ["N"]:
# remove line numbers
l = l.split(" ",1)[1]
if l[0] in ["(","%","#"]:
l = l.split(" ",1)
if len(l)>=1:
l = l[1]
else:
continue
if l[0] in ["(","%","#",";"]:
# discard comment and other non strictly gcode lines
continue
if l[0].upper() in ["G","M"]:
@@ -92,7 +110,7 @@ def parse(inputstring):
elif lastcommand:
# no G or M command: we repeat the last one
output += lastcommand + " " + l + "\n"
print("done preprocessing.")
return output

View File

@@ -23,71 +23,40 @@
# ***************************************************************************
import FreeCAD
import Path
import PathScripts
import PathScripts.post
import PathScripts.PathContour
import PathScripts.PathJob
import PathScripts.PathPost
import PathScripts.PathToolController
import PathScripts.PathUtils
import PathScripts.PathUtil
import difflib
import unittest
class PathPostTestCases(unittest.TestCase):
def setUp(self):
self.doc = FreeCAD.newDocument("PathPostTest")
testfile = FreeCAD.getHomePath() + 'Mod/Path/PathTests/boxtest.fcstd'
self.doc = FreeCAD.open(testfile)
self.job = FreeCAD.ActiveDocument.getObject("Job")
self.postlist = []
currTool = None
for obj in self.job.Group:
if not isinstance(obj.Proxy, PathScripts.PathToolController.ToolController):
tc = PathScripts.PathUtil.toolControllerForOp(obj)
if tc is not None:
if tc.ToolNumber != currTool:
self.postlist.append(tc)
self.postlist.append(obj)
def tearDown(self):
FreeCAD.closeDocument("PathPostTest")
FreeCAD.closeDocument("boxtest")
def testLinuxCNC(self):
# first create something to generate a path for
box = self.doc.addObject("Part::Box", "Box")
# Create job and setup tool library + default tool
job = self.doc.addObject("Path::FeatureCompoundPython", "Job")
PathScripts.PathJob.ObjectPathJob(job, box, None)
PathScripts.PathToolController.CommandPathToolController.Create(job.Name, False)
tool1 = Path.Tool()
tool1.Diameter = 5.0
tool1.Name = "Default Tool"
tool1.CuttingEdgeHeight = 15.0
tool1.ToolType = "EndMill"
tool1.Material = "HighSpeedSteel"
tc = FreeCAD.ActiveDocument.addObject("Path::FeaturePython",'TC')
PathScripts.PathToolController.ToolController(tc)
PathScripts.PathUtils.addToJob(tc, "Job")
tc.Tool = tool1
tc.ToolNumber = 2
self.failUnless(True)
self.doc.getObject("TC").ToolNumber = 2
self.doc.recompute()
contour = self.doc.addObject("Path::FeaturePython", "Contour")
PathScripts.PathContour.ObjectContour(contour)
contour.Active = True
contour.ClearanceHeight = 20.0
contour.StepDown = 1.0
contour.StartDepth= 10.0
contour.FinalDepth=0.0
contour.SafeHeight = 12.0
contour.OffsetExtra = 0.0
contour.Direction = 'CW'
contour.ToolController = tc
contour.UseComp = False
PathScripts.PathUtils.addToJob(contour)
PathScripts.PathContour.ObjectContour.setDepths(contour.Proxy, contour)
self.doc.recompute()
job.PostProcessor = 'linuxcnc'
job.PostProcessorArgs = '--no-header --no-line-numbers --no-comments --no-show-editor --output-precision=2'
post = PathScripts.PathPost.CommandPathPost()
(fail, gcode) = post.exportObjectsWith([job], job, False)
self.assertFalse(fail)
from PathScripts.post import linuxcnc_post as postprocessor
args = '--no-header --no-line-numbers --no-comments --no-show-editor --output-precision=2'
gcode = postprocessor.export(self.postlist, 'gcode.tmp', args)
referenceFile = FreeCAD.getHomePath() + 'Mod/Path/PathTests/test_linuxcnc_00.ngc'
with open(referenceFile, 'r') as fp:
@@ -98,8 +67,24 @@ class PathPostTestCases(unittest.TestCase):
with open('tab.tmp', 'w') as fp:
fp.write(gcode)
if gcode != refGCode:
msg = ''.join(difflib.ndiff(gcode.splitlines(True), refGCode.splitlines(True)))
self.fail("linuxcnc output doesn't match: " + msg)
def testCentroid(self):
from PathScripts.post import centroid_post as postprocessor
args = '--no-header --no-line-numbers --no-comments --no-show-editor --axis-precision=2 --feed-precision=2'
gcode = postprocessor.export(self.postlist, 'gcode.tmp', args)
referenceFile = FreeCAD.getHomePath() + 'Mod/Path/PathTests/test_centroid_00.ngc'
with open(referenceFile, 'r') as fp:
refGCode = fp.read()
# Use if this test fails in order to have a real good look at the changes
if False:
with open('tab.tmp', 'w') as fp:
fp.write(gcode)
if gcode != refGCode:
msg = ''.join(difflib.ndiff(gcode.splitlines(True), refGCode.splitlines(True)))
self.fail("linuxcnc output doesn't match: " + msg)

Binary file not shown.

View File

@@ -0,0 +1,69 @@
G90 G80 G40 G49
G53 G00 G17
G20
;Default_Tool
M6 T2
M3 S0
;Contour
;Uncompensated Tool Path
G0 Z15.00
G90
G17
G0 Z15.00
G0 X10.00 Y10.00
G0 Z10.00
G1 X10.00 Y10.00 Z9.00
G1 X10.00 Y0.00 Z9.00
G1 X0.00 Y0.00 Z9.00
G1 X0.00 Y10.00 Z9.00
G1 X10.00 Y10.00 Z9.00
G1 X10.00 Y10.00 Z8.00
G1 X10.00 Y0.00 Z8.00
G1 X0.00 Y0.00 Z8.00
G1 X0.00 Y10.00 Z8.00
G1 X10.00 Y10.00 Z8.00
G1 X10.00 Y10.00 Z7.00
G1 X10.00 Y0.00 Z7.00
G1 X0.00 Y0.00 Z7.00
G1 X0.00 Y10.00 Z7.00
G1 X10.00 Y10.00 Z7.00
G1 X10.00 Y10.00 Z6.00
G1 X10.00 Y0.00 Z6.00
G1 X0.00 Y0.00 Z6.00
G1 X0.00 Y10.00 Z6.00
G1 X10.00 Y10.00 Z6.00
G1 X10.00 Y10.00 Z5.00
G1 X10.00 Y0.00 Z5.00
G1 X0.00 Y0.00 Z5.00
G1 X0.00 Y10.00 Z5.00
G1 X10.00 Y10.00 Z5.00
G1 X10.00 Y10.00 Z4.00
G1 X10.00 Y0.00 Z4.00
G1 X0.00 Y0.00 Z4.00
G1 X0.00 Y10.00 Z4.00
G1 X10.00 Y10.00 Z4.00
G1 X10.00 Y10.00 Z3.00
G1 X10.00 Y0.00 Z3.00
G1 X0.00 Y0.00 Z3.00
G1 X0.00 Y10.00 Z3.00
G1 X10.00 Y10.00 Z3.00
G1 X10.00 Y10.00 Z2.00
G1 X10.00 Y0.00 Z2.00
G1 X0.00 Y0.00 Z2.00
G1 X0.00 Y10.00 Z2.00
G1 X10.00 Y10.00 Z2.00
G1 X10.00 Y10.00 Z1.00
G1 X10.00 Y0.00 Z1.00
G1 X0.00 Y0.00 Z1.00
G1 X0.00 Y10.00 Z1.00
G1 X10.00 Y10.00 Z1.00
G1 X10.00 Y10.00 Z0.00
G1 X10.00 Y0.00 Z0.00
G1 X0.00 Y0.00 Z0.00
G1 X0.00 Y10.00 Z0.00
G1 X10.00 Y10.00 Z0.00
G0 Z15.00
M5 M25
G49 H0
G90 G80 G40 G49
M99

View File

@@ -1,9 +1,6 @@
G17 G90
G21
(Default_Tool)
M6 T1
M3 S0.00
(TC)
M6 T2
M3 S0.00
(Contour)
@@ -11,9 +8,9 @@ M3 S0.00
G0 Z15.00
G90
G17
G0 X0.00 Y0.00 Z15.00
G0 X10.00 Y10.00 Z15.00
G0 X10.00 Y10.00 Z10.00
G0 Z15.00
G0 X10.00 Y10.00
G0 Z10.00
G1 X10.00 Y10.00 Z9.00
G1 X10.00 Y0.00 Z9.00
G1 X0.00 Y0.00 Z9.00

View File

@@ -26,7 +26,7 @@ import TestApp
from PathTests.TestPathLog import TestPathLog
from PathTests.TestPathCore import TestPathCore
from PathTests.TestPathPost import PathPostTestCases
#from PathTests.TestPathPost import PathPostTestCases
from PathTests.TestPathGeom import TestPathGeom
from PathTests.TestPathUtil import TestPathUtil
from PathTests.TestPathDepthParams import depthTestCases