From b195afc8ca2acc90775b0229f3f184d88edbaf47 Mon Sep 17 00:00:00 2001 From: Jon Nordby Date: Sat, 5 Sep 2015 00:53:42 +0200 Subject: [PATCH 1/4] Path: Fix unable to import from CLI Need to guard 'import FreeCADGui' in this case. --- src/Mod/Path/PathScripts/PathDrilling.py | 6 +++++- src/Mod/Path/PathScripts/PathMachine.py | 5 +++-- src/Mod/Path/PathScripts/PathPocket.py | 6 +++++- src/Mod/Path/PathScripts/PathProject.py | 6 +++++- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathDrilling.py b/src/Mod/Path/PathScripts/PathDrilling.py index 10bc9c0c1..9c12eba5c 100644 --- a/src/Mod/Path/PathScripts/PathDrilling.py +++ b/src/Mod/Path/PathScripts/PathDrilling.py @@ -22,10 +22,14 @@ #* * #*************************************************************************** -import FreeCAD,FreeCADGui,Path,PathGui +import FreeCAD,Path from PySide import QtCore,QtGui from PathScripts import PathUtils,PathSelection,PathProject +FreeCADGui = None +if FreeCAD.GuiUp: + import FreeCADGui + """Path Drilling object and FreeCAD command""" # Qt tanslation handling diff --git a/src/Mod/Path/PathScripts/PathMachine.py b/src/Mod/Path/PathScripts/PathMachine.py index dd39c93d3..15d1adc59 100644 --- a/src/Mod/Path/PathScripts/PathMachine.py +++ b/src/Mod/Path/PathScripts/PathMachine.py @@ -23,11 +23,11 @@ #*************************************************************************** ''' A CNC machine object to define how code is posted ''' -import FreeCAD,FreeCADGui,Path,PathGui +import FreeCAD,Path import PathScripts from PathScripts import PathProject, PathUtils from PySide import QtCore,QtGui -import os, sys +import os, sys # Qt tanslation handling try: @@ -228,6 +228,7 @@ class CommandPathMachine: if FreeCAD.GuiUp: # register the FreeCAD command + import FreeCADGui FreeCADGui.addCommand('Path_Machine',CommandPathMachine()) diff --git a/src/Mod/Path/PathScripts/PathPocket.py b/src/Mod/Path/PathScripts/PathPocket.py index 7b6522386..78029c75c 100644 --- a/src/Mod/Path/PathScripts/PathPocket.py +++ b/src/Mod/Path/PathScripts/PathPocket.py @@ -22,10 +22,14 @@ #* * #*************************************************************************** -import FreeCAD,FreeCADGui,Path,PathGui +import FreeCAD,Path from PySide import QtCore,QtGui from PathScripts import PathUtils +FreeCADGui = None +if FreeCAD.GuiUp: + import FreeCADGui + """Path Pocket object and FreeCAD command""" # Qt tanslation handling diff --git a/src/Mod/Path/PathScripts/PathProject.py b/src/Mod/Path/PathScripts/PathProject.py index 5b228a20b..a542253d2 100644 --- a/src/Mod/Path/PathScripts/PathProject.py +++ b/src/Mod/Path/PathScripts/PathProject.py @@ -22,9 +22,13 @@ #* * #*************************************************************************** -import FreeCAD,FreeCADGui,Path,PathGui +import FreeCAD,Path from PySide import QtCore,QtGui +FreeCADGui = None +if FreeCAD.GuiUp: + import FreeCADGui + """Path Project object and FreeCAD command""" # Qt tanslation handling From 6f89398b07414237b17657b771307ba5c458676b Mon Sep 17 00:00:00 2001 From: Jon Nordby Date: Sat, 5 Sep 2015 19:56:26 +0200 Subject: [PATCH 2/4] Path: Allow PostUtils to be used in CLI mode --- src/Mod/Path/PathScripts/PostUtils.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Mod/Path/PathScripts/PostUtils.py b/src/Mod/Path/PathScripts/PostUtils.py index 423e426b5..6377faead 100644 --- a/src/Mod/Path/PathScripts/PostUtils.py +++ b/src/Mod/Path/PathScripts/PostUtils.py @@ -27,9 +27,12 @@ These are a common functions and classes for creating custom post processors. ''' from PySide import QtCore, QtGui -import FreeCADGui import FreeCAD +FreeCADGui = None +if FreeCAD.GuiUp: + import FreeCADGui + class OldHighlighter(QtGui.QSyntaxHighlighter): def highlightBlock(self, text): myClassFormat = QtGui.QTextCharFormat() @@ -76,7 +79,9 @@ class GCodeHighlighter(QtGui.QSyntaxHighlighter): class GCodeEditorDialog(QtGui.QDialog): - def __init__(self, parent = FreeCADGui.getMainWindow()): + def __init__(self, parent = None): + if parent is None: + parent = FreeCADGui.getMainWindow() QtGui.QDialog.__init__(self,parent) layout = QtGui.QVBoxLayout(self) From 685865ce312579e8c2a260695c749748f84fe7b5 Mon Sep 17 00:00:00 2001 From: Jon Nordby Date: Sun, 6 Sep 2015 00:05:53 +0200 Subject: [PATCH 3/4] Draft: Fix typos in circleFrom2PointsRadius() Can't have been used in a while... --- src/Mod/Draft/DraftGeomUtils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Mod/Draft/DraftGeomUtils.py b/src/Mod/Draft/DraftGeomUtils.py index e99e75eeb..2391a2f80 100755 --- a/src/Mod/Draft/DraftGeomUtils.py +++ b/src/Mod/Draft/DraftGeomUtils.py @@ -2318,17 +2318,17 @@ def circleFrom2PointsRadius(p1, p2, radius): dist_p1p2 = DraftVecUtils.dist(p1, p1) mid = findMidpoint(p1_p2) if dist_p1p2 == 2*radius: - circle = Part.Circle(mid, norm, radius) + circle = Part.Circle(mid, NORM, radius) if circle: return [circle] else: return None dir = vec(p1_p2); dir.normalize() - perpDir = dir.cross(Vector(0,0,1)); perpDir.normailze() + perpDir = dir.cross(Vector(0,0,1)); perpDir.normalize() dist = math.sqrt(radius**2 - (dist_p1p2 / 2.0)**2) cen1 = Vector.add(mid, Vector(perpDir).multiply(dist)) cen2 = Vector.add(mid, Vector(perpDir).multiply(-dist)) circles = [] - if cen1: circles.append(Part.Circle(cen1, norm, radius)) - if cen2: circles.append(Part.Circle(cen2, norm, radius)) + if cen1: circles.append(Part.Circle(cen1, NORM, radius)) + if cen2: circles.append(Part.Circle(cen2, NORM, radius)) if circles: return circles else: return None From e81c4cf62a617f4ecc479ae69a0f71c1f91a9715 Mon Sep 17 00:00:00 2001 From: Jon Nordby Date: Sat, 5 Sep 2015 00:55:09 +0200 Subject: [PATCH 4/4] Path: Basic .rml postprocessor for Roland Modela MDX Jog, drill, linear and arc motions works. Tested on machine with some trivial projects, dimensions and feedrate speeds verified. Safe-Z/clearance command currently not respected (defaults to max height, super conservative). --- src/Mod/Path/CMakeLists.txt | 1 + src/Mod/Path/PathScripts/rml_post.py | 257 +++++++++++++++++++++++++++ 2 files changed, 258 insertions(+) create mode 100644 src/Mod/Path/PathScripts/rml_post.py diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index e66a3525f..bdd03c562 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -23,6 +23,7 @@ SET(PathScripts_SRCS PathScripts/centroid_post.py PathScripts/comparams_post.py PathScripts/dumper_post.py + PathScripts/rml_post.py PathScripts/TooltableEditor.py PathScripts/PathProfile.py PathScripts/PathPocket.py diff --git a/src/Mod/Path/PathScripts/rml_post.py b/src/Mod/Path/PathScripts/rml_post.py new file mode 100644 index 000000000..ac40fe1f3 --- /dev/null +++ b/src/Mod/Path/PathScripts/rml_post.py @@ -0,0 +1,257 @@ +#*************************************************************************** +#* (c) Jon Nordby (jononor@gmail.com) 2015 * +#* * +#* 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 * +#* * +#***************************************************************************/ + + +''' +FreeCAD Path post-processor to output code for the Roland Modela MDX-## machines. + +The machine speaks RML-1, specified in 'Roland RML-1 Programming Guidelines' +https://itp.nyu.edu/classes/cdp-spring2014/files/2014/09/RML1_Manual.pdf +http://altlab.org/d/content/m/pangelo/ideas/rml_command_guide_en_v100.pdf + +The format has some overlap with HPGL: +https://en.wikipedia.org/wiki/HPGL +http://paulbourke.net/dataformats/hpgl/ +''' + +import FreeCAD +import Part +import PostUtils + +# to distinguish python built-in open function from the one declared below +if open.__module__ == '__builtin__': + pythonopen = open + + +# Entrypoint used by FreeCAD +def export(objectslist, filename): + "Export objects as Roland Modela code." + + code = "" + for obj in objectslist: + code += convertobject(obj) + + gfile = pythonopen(filename,"wb") + gfile.write(code) + gfile.close() + +def convertobject(obj): + gcode = obj.Path.toGCode() + gcode = parse(gcode) + return gcode + +def motoron(): + return [ "!MC1;" ] +def motoroff(): + return [ "!MC0;" ] +def home(): + return [ "H;" ] + +def setjog(): + # "!PZ%d,%d;",iz_down,iz_up); // set z down, jog + return "" + +def addheader(): + return [ "PA;PA;" ] # absolute positioning +def addfooter(): + return [] + +def mm2cord(mm): + mm = float(mm) + return int(40.0*mm) + +def feed(x=None, y=None, z=None, state={}): + c = [] + + if x is not None: + x = float(x) + state['X'] = x + if y is not None: + y = float(y) + state['Y'] = y + if z is not None: + z = float(z) + state['Z'] = z + + if x is not None and y is not None and z is not None: + # 3d motion + c.append("Z%d,%d,%d;" % (mm2cord(x), mm2cord(y), mm2cord(z))) + elif x is not None and y is not None: + # 2d in XY plane + c.append("PD%d,%d;" % (mm2cord(x), mm2cord(y))) + elif z is not None: + pass # XXX: is this used? + return c + +def jog(x=None, y=None, z=None, state={}): + c = [] + if x is not None and y is not None: + x, y = float(x), float(y) + c.append("PU%d,%d;" % (mm2cord(x), mm2cord(y))) + state['X'] = x + state['Y'] = y + if z is not None: + z = float(z) + c.append("PU;") + # TODO: use !ZZ command + state['Z'] = z + + return c + +def xyarc(args, state): + # no native support in RML/Modela, convert to linear line segments + c = [] + + lastPoint = FreeCAD.Vector(state['X'], state['Y']) + newPoint = FreeCAD.Vector(float(args['X']), float(args['Y'])) + centerOffset = FreeCAD.Vector(float(args['I']), float(args['J'])) + center = lastPoint + centerOffset + radius = (center - lastPoint).Length + xyNormal = FreeCAD.Vector(0, 0, 1) + circle = Part.Circle(center, xyNormal, radius) + p0 = circle.parameter(lastPoint) + p1 = circle.parameter(newPoint) + arc = Part.ArcOfCircle(circle, p0, p1) + steps = 64 # TODO: specify max error instead + points = arc.discretize(steps) + # TODO: consider direction + #print 'p = Part.ArcOfCircle(Part.Circle(FreeCAD.Vector(%f, %f), FreeCAD.Vector(0, 0, 1), %f), %f, %f)' % (center.x, center.y, radius, p0, p1) + for p in points: + #print 'p', p.x, p.y + c += feed(p.x, p.y, state['Z'], state) + return c + +def speed(xy=None, z=None, state={}): + c = [] + print xy, z, state + if xy is not None: + xy = float(xy) + if xy > 0.0 and xy != state['XYspeed']: + c.append("VS%.1f;" % xy) + state['XYspeed'] = xy + if z is not None: + z = float(z) + if z > 0.0 and z != state['Zspeed']: + c.append("!VZ%.1f;" % z) + state['Zspeed'] = z + return c + +def convertgcode(cmd, args, state): + """Convert a single gcode command to equivalent Roland code""" + if cmd == 'G0': + # jog + return jog(args['X'], args['Y'], args['Z'], state) + elif cmd == 'G1': + # linear feed + c = [] + # feedrate + c += speed(xy=args['F'], z=args['F'], state=state) + # motion + c += feed(args['X'], args['Y'], args['Z'], state) + return c + elif cmd == 'G2' or cmd == 'G3': + # arc feed + c = [] + # feedrate + c += speed(xy=args['F'], state=state) + # motion + if args['X'] and args['Y'] and args['Z']: + # helical motion + pass + elif args['X'] and args['Y']: + # arc in plane + c += xyarc(args, state) + return c + elif cmd == 'G20': + # inches mode + raise ValueError("rml_post: Inches mode not supported") + elif cmd == 'G21': + # millimeter mode + return "" + elif cmd == 'G40': + # tool compensation off + return "" + elif cmd == 'G80': + # cancel all cycles (drill normally) + return "PU;" + elif cmd == 'G81': + c = [] + # feedrate + c += speed(z=args['F'], state=state) + # motion + c += jog(args['X'], args['Y'], state=state) + c += feed(args['X'], args['Y'], args['Z'], state) + return c + elif cmd == 'G90': + # absolute mode? + return "" + elif cmd == 'G98': + # feedrate + return "" + else: + raise NotImplementedError("rml_post: GCode command %s not understood" % (cmd,)) + + raise Exception("should not be reached. cmd=%s" % (cmd,)) + + +def parse(inputstring): + "parse(inputstring): returns a parsed output string" + + state = { 'X': 0.0, 'Y': 0.0, 'Z': 0.0, 'XYspeed': -1.0, 'Zspeed': -1.0 } + output = [] + + # header + output += addheader() + output += motoron() + + output += speed(2.0, 1.0, state) # defaults + + # TODO: respect clearance height + + # treat the input line by line + lines = inputstring.split("\n") + for line in lines: + if not line: + continue + parsed = PostUtils.stringsplit(line) + command = parsed['command'] + print 'cmd', line + try: + if command: + code = convertgcode(command, parsed, state) + if not isinstance(code, list): + code = [ code ] + if len(code) and code[0]: + output += code + except NotImplementedError, e: + print e + + # footer + output += motoroff() + output += home() + output += addfooter() + + return '\n'.join(output) + +print __name__ + " gcode postprocessor loaded." +