diff --git a/src/Mod/Path/App/AppPathPy.cpp b/src/Mod/Path/App/AppPathPy.cpp index 2c4832bfa6..a42721d3c5 100644 --- a/src/Mod/Path/App/AppPathPy.cpp +++ b/src/Mod/Path/App/AppPathPy.cpp @@ -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 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( diff --git a/src/Mod/Path/App/Area.cpp b/src/Mod/Path/App/Area.cpp index 9068e1f751..104c62b953 100644 --- a/src/Mod/Path/App/Area.cpp +++ b/src/Mod/Path/App/Area.cpp @@ -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 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();ipoints.size();ipstart()); - 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 sortWires(const gp_Pnt &pstart, gp_Pnt &pend,double min_dist) { + std::list 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 wires; if(min_dist < 0.01) min_dist = 0.01; @@ -2602,7 +2612,7 @@ struct WireOrienter { }; std::list Area::sortWires(const std::list &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 wires; @@ -2727,7 +2737,12 @@ std::list Area::sortWires(const std::list &shapes, AREA_TRACE("bound (" << xMin<<", "< Area::sortWires(const std::list &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 &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 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 &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 &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); diff --git a/src/Mod/Path/App/Area.h b/src/Mod/Path/App/Area.h index e2a88aa296..9d4d10eb01 100644 --- a/src/Mod/Path/App/Area.h +++ b/src/Mod/Path/App/Area.h @@ -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 sortWires(const std::list &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 * diff --git a/src/Mod/Path/App/FeaturePathShape.cpp b/src/Mod/Path/App/FeaturePathShape.cpp index 78e720c295..5d2c79ffad 100644 --- a/src/Mod/Path/App/FeaturePathShape.cpp +++ b/src/Mod/Path/App/FeaturePathShape.cpp @@ -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); } diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index bd68251899..a8a149cfc4 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -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 +) diff --git a/src/Mod/Path/PathScripts/PathContour.py b/src/Mod/Path/PathScripts/PathContour.py index e7cd85783a..9b071273b0 100644 --- a/src/Mod/Path/PathScripts/PathContour.py +++ b/src/Mod/Path/PathScripts/PathContour.py @@ -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() diff --git a/src/Mod/Path/PathScripts/PathMillFace.py b/src/Mod/Path/PathScripts/PathMillFace.py index b871058c20..8a7966fe94 100644 --- a/src/Mod/Path/PathScripts/PathMillFace.py +++ b/src/Mod/Path/PathScripts/PathMillFace.py @@ -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() diff --git a/src/Mod/Path/PathScripts/PathPocket.py b/src/Mod/Path/PathScripts/PathPocket.py index 1926bf9f4d..0bc9c435bd 100644 --- a/src/Mod/Path/PathScripts/PathPocket.py +++ b/src/Mod/Path/PathScripts/PathPocket.py @@ -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) diff --git a/src/Mod/Path/PathScripts/PathProfile.py b/src/Mod/Path/PathScripts/PathProfile.py index 71a7bf9d4c..66ea4ae60b 100644 --- a/src/Mod/Path/PathScripts/PathProfile.py +++ b/src/Mod/Path/PathScripts/PathProfile.py @@ -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) diff --git a/src/Mod/Path/PathScripts/PathProfileEdges.py b/src/Mod/Path/PathScripts/PathProfileEdges.py index d0bbb7f845..716c53a495 100644 --- a/src/Mod/Path/PathScripts/PathProfileEdges.py +++ b/src/Mod/Path/PathScripts/PathProfileEdges.py @@ -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) diff --git a/src/Mod/Path/PathScripts/post/centroid_post.py b/src/Mod/Path/PathScripts/post/centroid_post.py index 2977f9bc89..5c46077711 100644 --- a/src/Mod/Path/PathScripts/post/centroid_post.py +++ b/src/Mod/Path/PathScripts/post/centroid_post.py @@ -1,142 +1,338 @@ -# -*- coding: utf-8 -*- - -#*************************************************************************** -#* * -#* Copyright (c) 2015 Dan Falck * -#* * -#* 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 * +# * * +# * 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.") diff --git a/src/Mod/Path/PathScripts/post/example_pre.py b/src/Mod/Path/PathScripts/post/example_pre.py index fd06fb32ed..87d4b38ea9 100644 --- a/src/Mod/Path/PathScripts/post/example_pre.py +++ b/src/Mod/Path/PathScripts/post/example_pre.py @@ -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 diff --git a/src/Mod/Path/PathTests/TestPathPost.py b/src/Mod/Path/PathTests/TestPathPost.py index 7327ce0449..fbdfe67ae7 100644 --- a/src/Mod/Path/PathTests/TestPathPost.py +++ b/src/Mod/Path/PathTests/TestPathPost.py @@ -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) diff --git a/src/Mod/Path/PathTests/boxtest.fcstd b/src/Mod/Path/PathTests/boxtest.fcstd new file mode 100644 index 0000000000..fc8f2bf740 Binary files /dev/null and b/src/Mod/Path/PathTests/boxtest.fcstd differ diff --git a/src/Mod/Path/PathTests/test_centroid_00.ngc b/src/Mod/Path/PathTests/test_centroid_00.ngc new file mode 100644 index 0000000000..974c8a6250 --- /dev/null +++ b/src/Mod/Path/PathTests/test_centroid_00.ngc @@ -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 diff --git a/src/Mod/Path/PathTests/test_linuxcnc_00.ngc b/src/Mod/Path/PathTests/test_linuxcnc_00.ngc index 3fb2a21c47..6256f88731 100644 --- a/src/Mod/Path/PathTests/test_linuxcnc_00.ngc +++ b/src/Mod/Path/PathTests/test_linuxcnc_00.ngc @@ -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 diff --git a/src/Mod/Path/TestPathApp.py b/src/Mod/Path/TestPathApp.py index 531d5b163b..d0fad38cd0 100644 --- a/src/Mod/Path/TestPathApp.py +++ b/src/Mod/Path/TestPathApp.py @@ -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