Skip to content

Instantly share code, notes, and snippets.

@lava
Last active April 4, 2019 17:42
Show Gist options
  • Select an option

  • Save lava/84a58f84f811fbdab063d7cb5ea5148b to your computer and use it in GitHub Desktop.

Select an option

Save lava/84a58f84f811fbdab063d7cb5ea5148b to your computer and use it in GitHub Desktop.

Revisions

  1. lava revised this gist Apr 4, 2019. 1 changed file with 77 additions and 53 deletions.
    130 changes: 77 additions & 53 deletions easel_post.py
    Original file line number Diff line number Diff line change
    @@ -50,78 +50,80 @@ def export(objectslist, filename, argstring):
    print("the given object is not a path")
    return

    # The `Path.toGCode()` function returns a string formatted in the FreeCAD
    # internal GCode dialect
    #freecad_gcode = obj.Path.toGCode()
    easel_gcode = postprocess(obj.Path.Commands)
    gcode = postprocess(obj.Path.Commands)

    # Debugging info
    dia = PostUtils.GCodeEditorDialog()
    dia.editor.setText(easel_gcode)
    dia.editor.setText(gcode)
    result = dia.exec_()

    gfile = open(filename, "w")
    gfile.write(easel_gcode)
    gfile.write(gcode)
    gfile.close()

    return result


    def linearize_arc(start_dict, end_dict, center_dict, clockwise):
    def linearize_arc(start, end, center, normal, clockwise):
    result = "; {} arc from {} to {}\n; around center point {}\n".format(
    "Clockwise" if clockwise else "Counter-clockwise",
    start_dict,
    end_dict,
    center_dict)

    start = FreeCAD.Vector(start_dict['X'], start_dict['Y'], start_dict['Z'])
    end = FreeCAD.Vector(end_dict['X'], end_dict['Y'], end_dict['Z'])
    center = FreeCAD.Vector(center_dict['X'], center_dict['Y'], center_dict['Z'])
    if clockwise:
    start, end = end, start
    start,
    end,
    center)

    radius = (center - start).Length
    normal = (center - start).cross(center - end)
    # Special case: If the circle is under-specified (i.e. 180 or 360 degree angles),
    # we construct the normal
    # This ensures we're getting the z-axis as normal when start and center lie in the same z-plane.
    # This will break for circles on the xz or yz planes, but these don't make physical sense on a CNC mill.
    # TODO: Warn if we hit this condition.
    if (normal.Length == 0.):
    to_center = center - start
    reference = FreeCAD.Vector(to_center.y, -to_center.x, to_center.z)
    normal = to_center.cross(reference)
    normal.normalize()
    # TODO: Looks like we need to switch to the 3-point form to
    # reliably handle semi-circles and such.
    circle = Part.Circle(center, normal, radius)
    p0 = circle.parameter(start)
    p1 = circle.parameter(end)
    arc = Part.ArcOfCircle(circle, p0, p1)

    # Center everything at the origin for the following steps
    start0 = start - center
    end0 = end - center

    EPSILON = 1e-7
    if (start0 + end0).Length < EPSILON:
    # 180 degree angle
    mid0 = normal.cross(start0)
    elif (start0 - end0).Length < EPSILON:
    # 360 degree angle
    mid0 = -start0
    else:
    # "Normal" angle
    mid0 = radius*(start0 + end0).normalize()

    # Check if mid-point is clockwise or counter-clockwise from the start.
    mid_clockwise = normal.dot(start0.cross(mid0))

    # If it doesn't match the requested direction, choose the point on
    # the other side of the circle.
    if clockwise != mid_clockwise:
    mid0 = -mid0

    mid = center + mid0
    arc = Part.ArcOfCircle(start, mid, end)

    steps = 64 # TODO: make number of steps configurable
    points = arc.discretize(steps)
    c = []
    for p in points:
    c.append("G1 X{} Y{} Z{}\n".format(p.x, p.y, p.z))

    if clockwise:
    c.reverse()

    return result + "".join(c) + "\n"

    # `gcode`: [FreeCAD...Command]

    def postprocess(commands):
    output = "; Generated by FreeCAD Easel post-processor.\n"
    output += "G21 ; Metric units\n" # FreeCAD internal values are always metric.

    # Currently supported internal commands:
    # G0, G1, G2, G3 (movement commands)
    # G81, G82, G83 (drilling commands)
    # G90, G91 (absolute/relative coordinates)
    # G0, G1, G2, G3 (Movement)
    # G17, G18, G19 (Plane selection for G2/G3 arcs)
    # G81, G82, G83 (Drilling)
    # G90, G91 (Absolute/relative coordinates)

    # Carvey starts at (20mm, 20mm, 20mm) after smart-clamp calibration.
    # TODO: Check if this is different for the other easel-supported machines.
    # TODO: Turn `state` into a FreeCAD.Vector ?
    state = {'X': 20., 'Y': 20., 'Z': 20.}
    normal = FreeCAD.Vector(0, 0, 1)

    for cmd in commands:
    if cmd.Name[0] == "(":
    @@ -137,32 +139,44 @@ def postprocess(commands):
    state['Y'] = float(cmd.Parameters['Y'])
    if 'Z' in argnames:
    state['Z'] = float(cmd.Parameters['Z'])
    # TODO: Is it important to omit duplicate coordinates?

    # We could make generated file sizes slightly smaller by omitting
    # unchanged coordinates, but so far we haven't hit a size limit in
    # Easel.
    output += "{} X{} Y{} Z{}\n".format(cmd.Name, state['X'], state['Y'], state['Z'])

    elif cmd.Name == "G2" or cmd.Name == "G3":
    # We are guaranteed that the arc described by these commands always
    # lies in one of the three coordinate planes, and that the correct
    # plane has already been set through the G17/G18/G19 commands.
    # For arcs that are not aligned to a plane, FreeCAD will linearize
    # them internally and generate a sequence of G1 movements itself.

    argnames = cmd.Parameters.keys()
    if 'A' in argnames or 'B' in argnames or 'C' in argnames:
    raise RuntimeError("A,B,C rotational axes currently not supported.")

    center_x = state['X'] + cmd.Parameters['I']
    center_y = state['Y'] + cmd.Parameters['J']
    center_z = state['Z']
    if 'K' in argnames: # 'K' is optional
    center_z = state['Z'] + cmd.Parameters['K']

    # Update final position
    start_position = dict(state)
    end_position = dict(state)
    start = FreeCAD.Vector(state['X'], state['Y'], state['Z'])
    end = FreeCAD.Vector(state['X'], state['Y'], state['Z'])
    center = FreeCAD.Vector(state['X'], state['Y'], state['Z'])

    if 'X' in argnames:
    end_position['X'] = float(cmd.Parameters['X'])
    end.x = float(cmd.Parameters['X'])
    if 'Y' in argnames:
    end_position['Y'] = float(cmd.Parameters['Y'])
    end.y = float(cmd.Parameters['Y'])
    if 'Z' in argnames:
    end_position['Z'] = float(cmd.Parameters['Z'])
    end.z = float(cmd.Parameters['Z'])
    if 'I' in argnames:
    center.x += float(cmd.Parameters['I'])
    if 'J' in argnames:
    center.y += float(cmd.Parameters['J'])
    if 'K' in argnames:
    center.z += float(cmd.Parameters['K'])

    clockwise = cmd.Name == "G2"
    center = {'X': center_x, 'Y': center_y, 'Z': center_z}
    output += linearize_arc(start_position, end_position, center, clockwise)

    output += linearize_arc(start, end, center, normal, clockwise)

    state = end_position
    elif cmd.Name == "G81" or cmd.Name == "G82" or cmd.Name == "G83":
    @@ -171,6 +185,16 @@ def postprocess(commands):
    output += "G90 ; Absolute coordinate mode\n"
    elif cmd.Name == "G91":
    raise RuntimeError("Relative coordinate mode (G91) currently not supported.")
    elif cmd.Name == "G17": # Use the xy-plane for arc movements.
    normal = FreeCAD.Vector(0, 0, 1)
    elif cmd.Name == "G18": # Use the zx-plane for arc movements.
    normal = FreeCAD.Vector(0, 1, 0)
    elif cmd.Name == "G19": # Use the yz-plane for arc movements.
    normal = FreeCAD.Vector(1, 0, 0)
    else:
    # TODO: Maybe just output unknown commands unchanged?
    raise RuntimeError("Unexpected internal G-Code command {}".format(cmd.Name))


    return output

  2. lava revised this gist Apr 4, 2019. 1 changed file with 109 additions and 14 deletions.
    123 changes: 109 additions & 14 deletions easel_post.py
    Original file line number Diff line number Diff line change
    @@ -34,13 +34,9 @@

    import datetime

    # TODO(bevers): Stuff below was in the original example code, but I don't
    # see why it is needed.
    #
    # to distinguish python built-in open function from the one declared below
    #if open.__module__ in ['__builtin__','io']:
    # pythonopen = open

    import FreeCAD
    import Part
    import PathScripts.PostUtils as PostUtils


    def export(objectslist, filename, argstring):
    @@ -52,6 +48,7 @@ def export(objectslist, filename, argstring):
    obj = objectslist[0]
    if not hasattr(obj, "Path"):
    print("the given object is not a path")
    return

    # The `Path.toGCode()` function returns a string formatted in the FreeCAD
    # internal GCode dialect
    @@ -62,20 +59,118 @@ def export(objectslist, filename, argstring):
    dia.editor.setText(easel_gcode)
    result = dia.exec_()

    #gfile = open(filename, "w")
    #gfile.write(easel_gcode)
    #gfile.close()
    gfile = open(filename, "w")
    gfile.write(easel_gcode)
    gfile.close()

    return result


    # `gcode`: [string]
    def linearize_arc(start_dict, end_dict, center_dict, clockwise):
    result = "; {} arc from {} to {}\n; around center point {}\n".format(
    "Clockwise" if clockwise else "Counter-clockwise",
    start_dict,
    end_dict,
    center_dict)

    start = FreeCAD.Vector(start_dict['X'], start_dict['Y'], start_dict['Z'])
    end = FreeCAD.Vector(end_dict['X'], end_dict['Y'], end_dict['Z'])
    center = FreeCAD.Vector(center_dict['X'], center_dict['Y'], center_dict['Z'])
    if clockwise:
    start, end = end, start

    radius = (center - start).Length
    normal = (center - start).cross(center - end)
    # Special case: If the circle is under-specified (i.e. 180 or 360 degree angles),
    # we construct the normal
    # This ensures we're getting the z-axis as normal when start and center lie in the same z-plane.
    # This will break for circles on the xz or yz planes, but these don't make physical sense on a CNC mill.
    # TODO: Warn if we hit this condition.
    if (normal.Length == 0.):
    to_center = center - start
    reference = FreeCAD.Vector(to_center.y, -to_center.x, to_center.z)
    normal = to_center.cross(reference)
    normal.normalize()
    # TODO: Looks like we need to switch to the 3-point form to
    # reliably handle semi-circles and such.
    circle = Part.Circle(center, normal, radius)
    p0 = circle.parameter(start)
    p1 = circle.parameter(end)
    arc = Part.ArcOfCircle(circle, p0, p1)

    steps = 64 # TODO: make number of steps configurable
    points = arc.discretize(steps)
    c = []
    for p in points:
    c.append("G1 X{} Y{} Z{}\n".format(p.x, p.y, p.z))

    if clockwise:
    c.reverse()

    return result + "".join(c) + "\n"

    # `gcode`: [FreeCAD...Command]
    def postprocess(commands):
    output = ""
    output = "; Generated by FreeCAD Easel post-processor.\n"
    output += "G21 ; Metric units\n" # FreeCAD internal values are always metric.

    # Currently supported internal commands:
    # G0, G1, G2, G3 (movement commands)
    # G81, G82, G83 (drilling commands)
    # G90, G91 (absolute/relative coordinates)

    # Carvey starts at (20mm, 20mm, 20mm) after smart-clamp calibration.
    # TODO: Check if this is different for the other easel-supported machines.
    state = {'X': 20., 'Y': 20., 'Z': 20.}

    for cmd in commands:
    output += str(print(cmd.Name)) + "\n"
    output += str(dir(cmd)) + "\n"
    if cmd.Name[0] == "(":
    output += "; " + cmd.Name + "\n"
    elif cmd.Name == "G0" or cmd.Name == "G1":
    argnames = cmd.Parameters.keys()
    if 'A' in argnames or 'B' in argnames or 'C' in argnames:
    raise RuntimeError("A,B,C rotational axes currently not supported.")

    if 'X' in argnames:
    state['X'] = float(cmd.Parameters['X'])
    if 'Y' in argnames:
    state['Y'] = float(cmd.Parameters['Y'])
    if 'Z' in argnames:
    state['Z'] = float(cmd.Parameters['Z'])
    # TODO: Is it important to omit duplicate coordinates?
    output += "{} X{} Y{} Z{}\n".format(cmd.Name, state['X'], state['Y'], state['Z'])
    elif cmd.Name == "G2" or cmd.Name == "G3":
    argnames = cmd.Parameters.keys()
    if 'A' in argnames or 'B' in argnames or 'C' in argnames:
    raise RuntimeError("A,B,C rotational axes currently not supported.")

    center_x = state['X'] + cmd.Parameters['I']
    center_y = state['Y'] + cmd.Parameters['J']
    center_z = state['Z']
    if 'K' in argnames: # 'K' is optional
    center_z = state['Z'] + cmd.Parameters['K']

    # Update final position
    start_position = dict(state)
    end_position = dict(state)
    if 'X' in argnames:
    end_position['X'] = float(cmd.Parameters['X'])
    if 'Y' in argnames:
    end_position['Y'] = float(cmd.Parameters['Y'])
    if 'Z' in argnames:
    end_position['Z'] = float(cmd.Parameters['Z'])

    clockwise = cmd.Name == "G2"
    center = {'X': center_x, 'Y': center_y, 'Z': center_z}
    output += linearize_arc(start_position, end_position, center, clockwise)

    state = end_position
    elif cmd.Name == "G81" or cmd.Name == "G82" or cmd.Name == "G83":
    raise RuntimeError("Drilling commands (G8x) currently not supported.")
    elif cmd.Name == "G90":
    output += "G90 ; Absolute coordinate mode\n"
    elif cmd.Name == "G91":
    raise RuntimeError("Relative coordinate mode (G91) currently not supported.")

    return output

  3. lava created this gist Apr 3, 2019.
    82 changes: 82 additions & 0 deletions easel_post.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,82 @@
    # ***************************************************************************
    # * (c) Benno Evers (bennoe@apache.org) 2019 *
    # * *
    # * 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='''
    This is a post-processor for the Easel WebUI which can control various
    models of CNC mills, i.e. Carvey, X-Carve and ShapeOko 1/2.
    Note that it has only been tested with the Carvey.
    Its main task is to linearize any arcs, since Easel only
    supports linear movements.
    '''

    import datetime

    # TODO(bevers): Stuff below was in the original example code, but I don't
    # see why it is needed.
    #
    # to distinguish python built-in open function from the one declared below
    #if open.__module__ in ['__builtin__','io']:
    # pythonopen = open



    def export(objectslist, filename, argstring):
    "Called when freecad exports a list of objects"
    # TODO(bevers): Support multiple paths by concatenating them together.
    if len(objectslist) > 1:
    print("This script is unable to write more than one Path object")
    return
    obj = objectslist[0]
    if not hasattr(obj, "Path"):
    print("the given object is not a path")

    # The `Path.toGCode()` function returns a string formatted in the FreeCAD
    # internal GCode dialect
    #freecad_gcode = obj.Path.toGCode()
    easel_gcode = postprocess(obj.Path.Commands)

    dia = PostUtils.GCodeEditorDialog()
    dia.editor.setText(easel_gcode)
    result = dia.exec_()

    #gfile = open(filename, "w")
    #gfile.write(easel_gcode)
    #gfile.close()

    return result


    # `gcode`: [string]
    def postprocess(commands):
    output = ""

    for cmd in commands:
    output += str(print(cmd.Name)) + "\n"
    output += str(dir(cmd)) + "\n"

    return output

    print(__name__ + " gcode postprocessor loaded.")