[Solved] I need help for scripting 3D laser engraving paths

Here's the place for discussion related to CAM/CNC and the development of the Path module.
Forum rules
Be nice to others! Respect the FreeCAD code of conduct!
kovacevic39
Posts: 3
Joined: Mon Dec 20, 2021 4:40 pm

Re: I need help for scripting 3D laser engraving paths

Post by kovacevic39 »

@PhilippDahlem

You need to add a few lines of code to use the postprocessor with your script:
-Import the postprocessor
-Add the path object to a list
-Export the gcode with the postprocessor

I've added these below.

Code: Select all

import FreeCAD
import Path
import laser_post #import  postprocessor


shapes = FreeCADGui.Selection.getSelectionEx()[0].SubObjects
o = FreeCAD.ActiveDocument.addObject("Path::Feature","LaserPath")
o.Path = Path.fromShapes(shapes, sort_mode=2, min_dist=1.0, abscissa=3.0, nearest_k=3, orientation=0, direction=0, threshold=2.0, retract_axis=2, retraction=70.0, resume_height=70.0, segmentation=0.0, feedrate=0.0, feedrate_v=0.0, deflection=0.01)
FreeCAD.ActiveDocument.recompute()

object_list = [o] #the postprocessor expects a list of path objects

laser_post.export(object_list,"laser_gcode.nc", "--laser-on=M3;S900 --laser-3d") #export gcode with the postprocessor

I've tested the script above with some simple shapes and it seems to work.
If you change the command line options you may need to restart FreeCAD to avoid strange results.

Also, make sure you have the most recent version.

https://github.com/kovacevic39/laser_post

Let me know if this helps.

Happy New Year.
PhilippDahlem
Posts: 4
Joined: Sat Dec 18, 2021 3:39 pm

Re: I need help for scripting 3D laser engraving paths

Post by PhilippDahlem »

Hi kovacevic39,

please apologize my late response. I've tested your postprocessor and it works as you said. Thanks a lot for your great support! Your script was also a perfect starting point for me to get into the postprocessor syntax, so that i could program one myself, which is optimized for my needs, as i wrote in my last post. Finally, i end up with a short laser_gcode() function, which i added to the grbl postprocessor, similar as you did.

Now, i can fully avoid the retraction movements, depending on the gap distance between selected edges, which i define in a gap treshold parameter.

In the picture below i think i becomes clear, why the retraction movements are annoying me. They simply blow up the processing time.
Laser_gcode_withretractionmovements.png
Laser_gcode_withretractionmovements.png (234.62 KiB) Viewed 951 times
After the postprocessing, now, big gaps will lead to a retraction movement just out of G0 commands so the laser is turned off, and small gaps will lead to one G0 movement without retraction at all.
Laser_gcode_afterpostprocessing.png
Laser_gcode_afterpostprocessing.png (44.67 KiB) Viewed 951 times
Here you can see a first result. I still need to tune the laser parameters, but this is a another topic ;-)
Laser_gcode_firstresult.png
Laser_gcode_firstresult.png (275.14 KiB) Viewed 951 times
Here is the postprocessor code, which i like to share. For me it works like a charm.

Code: Select all

# -*- coding: utf-8 -*-
# ***************************************************************************
# *   Copyright (c) 2014 sliptonic <shopinthewoods@gmail.com>               *
# *   Copyright (c) 2018, 2019 Gauthier Briere                              *
# *   Copyright (c) 2019, 2020 Schildkroet                                  *
# *                                                                         *
# *   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                                                                   *
# *                                                                         *
# ***************************************************************************

import FreeCAD
from FreeCAD import Units
import PathScripts.PostUtils as PostUtils
import argparse
import datetime
import shlex
import math
import PathScripts.PathUtil as PathUtil


TOOLTIP = '''
Generate g-code from a Path that is compatible with the grbl controller.
import grbl_post
grbl_post.export(object, "/path/to/file.ncc")
'''


# ***************************************************************************
# * Globals set customization preferences
# ***************************************************************************

# Default values for command line arguments:
OUTPUT_COMMENTS = True            # default output of comments in output gCode file
OUTPUT_HEADER = True              # default output header in output gCode file
OUTPUT_LINE_NUMBERS = False       # default doesn't output line numbers in output gCode file
OUTPUT_BCNC = True               # default doesn't add bCNC operation block headers in output gCode file
SHOW_EDITOR = True                # default show the resulting file dialog output in GUI
PRECISION = 3                     # Default precision for metric (see http://linuxcnc.org/docs/2.7/html/gcode/overview.html#_g_code_best_practices)
TRANSLATE_DRILL_CYCLES = False    # If true, G81, G82 & G83 are translated in G0/G1 moves
PREAMBLE = '''G17 G90
'''                               # default preamble text will appear at the beginning of the gCode output file.
POSTAMBLE = '''M5
G17 G90
M2
'''                               # default postamble text will appear following the last operation.

SPINDLE_WAIT = 0                  # no waiting after M3 / M4 by default
RETURN_TO = None                  # no movements after end of program

# Customisation with no command line argument
MODAL = False                     # if true commands are suppressed if the same as previous line.
LINENR = 100                      # line number starting value
LINEINCR = 10                     # line number increment
OUTPUT_TOOL_CHANGE = False        # default don't output M6 tool changes (comment it) as grbl currently does not handle it
DRILL_RETRACT_MODE = 'G98'        # Default value of drill retractations (CURRENT_Z) other possible value is G99
MOTION_MODE = 'G90'               # G90 for absolute moves, G91 for relative
UNITS = 'G21'                     # G21 for metric, G20 for us standard
UNIT_FORMAT = 'mm'
UNIT_SPEED_FORMAT = 'mm/min'
PRE_OPERATION = ''''''            # Pre operation text will be inserted before every operation
POST_OPERATION = ''''''           # Post operation text will be inserted after every operation
TOOL_CHANGE = ''''''              # Tool Change commands will be inserted before a tool change

# ***************************************************************************
# * End of customization
# ***************************************************************************

# Parser arguments list & definition
parser = argparse.ArgumentParser(prog='grbl', add_help=False)
parser.add_argument('--comments',           action='store_true', help='output comment (default)')
parser.add_argument('--no-comments',        action='store_true', help='suppress comment output')
parser.add_argument('--header',             action='store_true', help='output headers (default)')
parser.add_argument('--no-header',          action='store_true', help='suppress header output')
parser.add_argument('--line-numbers',       action='store_true', help='prefix with line numbers')
parser.add_argument('--no-line-numbers',    action='store_true', help='don\'t prefix with line numbers (default)')
parser.add_argument('--show-editor',        action='store_true', help='pop up editor before writing output (default)')
parser.add_argument('--no-show-editor',     action='store_true', help='don\'t pop up editor before writing output')
parser.add_argument('--precision',          default='3',         help='number of digits of precision, default=3')
parser.add_argument('--translate_drill',    action='store_true', help='translate drill cycles G81, G82 & G83 in G0/G1 movements')
parser.add_argument('--no-translate_drill', action='store_true', help='don\'t translate drill cycles G81, G82 & G83 in G0/G1 movements (default)')
parser.add_argument('--preamble',                                help='set commands to be issued before the first command, default="G17 G90"')
parser.add_argument('--postamble',                               help='set commands to be issued after the last command, default="M5\nG17 G90\n;M2"')
parser.add_argument('--inches',             action='store_true', help='Convert output for US imperial mode (G20)')
parser.add_argument('--tool-change',        action='store_true', help='Insert M6 for all tool changes')
parser.add_argument('--wait-for-spindle',   type=int, default=0, help='Wait for spindle to reach desired speed after M3 / M4, default=0')
parser.add_argument('--return-to',          default='',          help='Move to the specified coordinates at the end, e.g. --return-to=0,0')
parser.add_argument('--bcnc',               action='store_true', help='Add Job operations as bCNC block headers. Consider suppressing existing comments: Add argument --no-comments')
parser.add_argument('--no-bcnc',            action='store_true', help='suppress bCNC block header output (default)')
parser.add_argument('--gap-treshold', help='define the minimal gap distance between select pathes for using retraction movements')
TOOLTIP_ARGS = parser.format_help()


# ***************************************************************************
# * Internal global variables
# ***************************************************************************
MOTION_COMMANDS = ['G0', 'G00', 'G1', 'G01', 'G2', 'G02', 'G3', 'G03']  # Motion gCode commands definition
RAPID_MOVES = ['G0', 'G00']                                             # Rapid moves gCode commands definition
SUPPRESS_COMMANDS = []                                      # These commands are ignored by commenting them out
COMMAND_SPACE = " "
# Global variables storing current position
CURRENT_X = 0
CURRENT_Y = 0
CURRENT_Z = 0
GAP_TRESHOLD = 20

# ***************************************************************************
# * to distinguish python built-in open function from the one declared below
if open.__module__ in ['__builtin__', 'io']:
  pythonopen = open


def processArguments(argstring):

  global OUTPUT_HEADER
  global OUTPUT_COMMENTS
  global OUTPUT_LINE_NUMBERS
  global SHOW_EDITOR
  global PRECISION
  global PREAMBLE
  global POSTAMBLE
  global UNITS
  global UNIT_SPEED_FORMAT
  global UNIT_FORMAT
  global TRANSLATE_DRILL_CYCLES
  global OUTPUT_TOOL_CHANGE
  global SPINDLE_WAIT
  global RETURN_TO
  global OUTPUT_BCNC
  global GAP_TRESHOLD

  try:
    args = parser.parse_args(shlex.split(argstring))
    if args.no_header:
      OUTPUT_HEADER = False
    if args.header:
      OUTPUT_HEADER = True
    if args.no_comments:
      OUTPUT_COMMENTS = False
    if args.comments:
      OUTPUT_COMMENTS = True
    if args.no_line_numbers:
      OUTPUT_LINE_NUMBERS = False
    if args.line_numbers:
      OUTPUT_LINE_NUMBERS = True
    if args.no_show_editor:
      SHOW_EDITOR = False
    if args.show_editor:
      SHOW_EDITOR = True
    PRECISION = args.precision
    if args.preamble is not None:
      PREAMBLE = args.preamble
    if args.postamble is not None:
      POSTAMBLE = args.postamble
    if args.no_translate_drill:
      TRANSLATE_DRILL_CYCLES = False
    if args.translate_drill:
      TRANSLATE_DRILL_CYCLES = True
    if args.inches:
      UNITS = 'G20'
      UNIT_SPEED_FORMAT = 'in/min'
      UNIT_FORMAT = 'in'
      PRECISION = 4
    if args.tool_change:
      OUTPUT_TOOL_CHANGE = True
    if args.wait_for_spindle > 0:
      SPINDLE_WAIT = args.wait_for_spindle
    if args.return_to != '':
      RETURN_TO = [int(v) for v in args.return_to.split(',')]
      if len(RETURN_TO) != 2:
        RETURN_TO = None
        print("--return-to coordinates must be specified as <x>,<y>, ignoring")
    if args.bcnc:
      OUTPUT_BCNC = True
    if args.no_bcnc:
      OUTPUT_BCNC = False
    if args.gap_treshold is not None:
      GAP_TRESHOLD = args.gap_treshold


  except Exception as e:
    return False

  return True


# For debug...
def dump(obj):
  for attr in dir(obj):
    print("obj.%s = %s" % (attr, getattr(obj, attr)))


def export(objectslist, filename, argstring):

  if not processArguments(argstring):
    return None

  global UNITS
  global UNIT_FORMAT
  global UNIT_SPEED_FORMAT
  global MOTION_MODE
  global SUPPRESS_COMMANDS

  print("Post Processor: " + __name__ + " postprocessing...")
  gcode = ""

  # write header
  if OUTPUT_HEADER:
    gcode += linenumber() + "(Exported by FreeCAD)\n"
    gcode += linenumber() + "(Post Processor: " + __name__ + ")\n"
    gcode += linenumber() + "(Output Time:" + str(datetime.datetime.now()) + ")\n"

  # Check canned cycles for drilling
  if TRANSLATE_DRILL_CYCLES:
    if len(SUPPRESS_COMMANDS) == 0:
      SUPPRESS_COMMANDS = ['G99', 'G98', 'G80']
    else:
      SUPPRESS_COMMANDS += ['G99', 'G98', 'G80']

  # Write the preamble
  if OUTPUT_COMMENTS:
    gcode += linenumber() + "(Begin preamble)\n"
  for line in PREAMBLE.splitlines(True):
    gcode += linenumber() + line
  # verify if PREAMBLE have changed MOTION_MODE or UNITS
  if 'G90' in PREAMBLE:
    MOTION_MODE = 'G90'
  elif 'G91' in PREAMBLE:
    MOTION_MODE = 'G91'
  else:
    gcode += linenumber() + MOTION_MODE + "\n"
  if 'G21' in PREAMBLE:
    UNITS = 'G21'
    UNIT_FORMAT = 'mm'
    UNIT_SPEED_FORMAT = 'mm/min'
  elif 'G20' in PREAMBLE:
    UNITS = 'G20'
    UNIT_FORMAT = 'in'
    UNIT_SPEED_FORMAT = 'in/min'
  else:
    gcode += linenumber() + UNITS + "\n"

  for obj in objectslist:
    # Debug...
    # print("\n" + "*"*70)
    # dump(obj)
    # print("*"*70 + "\n")
    if not hasattr(obj, "Path"):
      print("The object " + obj.Name + " is not a path. Please select only path and Compounds.")
      return

    # Skip inactive operations
    if PathUtil.opProperty(obj, 'Active') is False:
        continue

    # do the pre_op
    if OUTPUT_BCNC:
      gcode += linenumber() + "(Block-name: " + obj.Label + ")\n"
      gcode += linenumber() + "(Block-expand: 0)\n"
      gcode += linenumber() + "(Block-enable: 1)\n"
    if OUTPUT_COMMENTS:
      gcode += linenumber() + "(Begin operation: " + obj.Label + ")\n"
    for line in PRE_OPERATION.splitlines(True):
      gcode += linenumber() + line

    # get coolant mode
    coolantMode = 'None'
    if hasattr(obj, "CoolantMode") or hasattr(obj, 'Base') and  hasattr(obj.Base, "CoolantMode"):
        if hasattr(obj, "CoolantMode"):
            coolantMode = obj.CoolantMode
        else:
            coolantMode = obj.Base.CoolantMode

    # turn coolant on if required
    if OUTPUT_COMMENTS:
        if not coolantMode == 'None':
            gcode += linenumber() + '(Coolant On:' + coolantMode + ')\n'
    if coolantMode == 'Flood':
        gcode  += linenumber() + 'M8' + '\n'
    if coolantMode == 'Mist':
        gcode += linenumber() + 'M7' + '\n'

    # Parse the op
    gcode += parse(obj)

    # do the post_op
    if OUTPUT_COMMENTS:
      gcode += linenumber() + "(Finish operation: " + obj.Label + ")\n"
    for line in POST_OPERATION.splitlines(True):
      gcode += linenumber() + line

    # turn coolant off if required
    if not coolantMode == 'None':
        if OUTPUT_COMMENTS:
            gcode += linenumber() + '(Coolant Off:' + coolantMode + ')\n'
        gcode += linenumber() +'M9' + '\n'

  if RETURN_TO:
    gcode += linenumber() + "G0 X%s Y%s\n" % tuple(RETURN_TO)

  # do the post_amble
  if OUTPUT_BCNC:
    gcode += linenumber() + "(Block-name: post_amble)\n"
    gcode += linenumber() + "(Block-expand: 0)\n"
    gcode += linenumber() + "(Block-enable: 1)\n"
  if OUTPUT_COMMENTS:
    gcode += linenumber() + "(Begin postamble)\n"
  for line in POSTAMBLE.splitlines(True):
    gcode += linenumber() + line

  # show the gCode result dialog
  if FreeCAD.GuiUp and SHOW_EDITOR:
    dia = PostUtils.GCodeEditorDialog()
    dia.editor.setText(gcode)
    result = dia.exec_()
    if result:
      final = dia.editor.toPlainText()
    else:
      final = gcode
  else:
    final = gcode

  laser_gcode(gcode, filename)
    
def get_position(gcode_line):
  position = []
  line_elements = gcode_line.split(" ")
  for parameter in line_elements:
    if 'X' in parameter:
      position.append(float(parameter.lstrip("XYZ")))
    elif 'Y' in parameter:
      position.append(float(parameter.lstrip("XYZ")))
    elif 'Z' in parameter:
      position.append(float(parameter.lstrip("XYZ")))
  return position

def laser_gcode(gcode, filename):
  global GAP_TRESHOLD

  gfile = open(filename, "w")
  small_gap = 0
  big_gap = 0
  length =len(gcode.splitlines(True))
  
  for i,line in enumerate(gcode.splitlines(True)): # Reprocess lines.

    if small_gap > 1 and big_gap == 0: # muss bei 4 starten
      small_gap -= 1
      continue
    elif small_gap == 1 and big_gap == 0:
      newline = line.replace("G1","G0")
      small_gap -= 1
    elif small_gap == 0 and big_gap > 2:
      big_gap -= 1
      newline=line
    elif small_gap == 0 and big_gap == 2:
      big_gap -= 1
      continue
    elif small_gap == 0 and big_gap == 1:
      newline = line.replace("G1","G0")
      big_gap -= 1  
    elif 'G0' not in gcode.splitlines(True)[i-3] and 'G0' in gcode.splitlines(True)[i-2] and 'G0' in gcode.splitlines(True)[i-1] and 'G1' in line: #if "G0" occurs two times then there is the "start gap"
      newline = line.replace("G1","G0")
    else:
      newline=line
      if i < length-4:
        if 'G0' in gcode.splitlines(True)[i+1] and 'G0' in gcode.splitlines(True)[i+2] and 'G0' in gcode.splitlines(True)[i+3]: #if "G0" occurs three times then there is a gap
          position_before_gap=get_position(line)
          position_after_gap=get_position(gcode.splitlines(True)[i+4])
          gap=math.sqrt((position_after_gap[0]-position_before_gap[0])**2+(position_after_gap[1]-position_before_gap[1])**2+(position_after_gap[2]-position_before_gap[2])**2)
          if gap >= GAP_TRESHOLD:
            big_gap=4
          elif gap < GAP_TRESHOLD:
            small_gap=4
            
        
      
    gfile.write(newline) 
    
  gfile.close()

  print("Done postprocessing.")


def linenumber():
  if not OUTPUT_LINE_NUMBERS:
    return ""
  global LINENR
  global LINEINCR
  s = "N" + str(LINENR) + " "
  LINENR += LINEINCR
  return s


def format_outstring(strTable):
  global COMMAND_SPACE
  # construct the line for the final output
  s = ""
  for w in strTable:
    s += w + COMMAND_SPACE
  s = s.strip()
  return s


def parse(pathobj):

  global DRILL_RETRACT_MODE
  global MOTION_MODE
  global CURRENT_X
  global CURRENT_Y
  global CURRENT_Z

  out = ""
  lastcommand = None
  precision_string = '.' + str(PRECISION) + 'f'

  params = ['X', 'Y', 'Z', 'A', 'B', 'C', 'U', 'V', 'W', 'I', 'J', 'K', 'F', 'S', 'T', 'Q', 'R', 'L', 'P']

  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
    if not hasattr(pathobj, "Path"):  # groups might contain non-path things like stock.
      return out

    if OUTPUT_COMMENTS:
      out += linenumber() + "(Path: " + pathobj.Label + ")\n"

    for c in pathobj.Path.Commands:
      outstring = []
      command = c.Name

      outstring.append(command)

      # if modal: only print the command if it is not the same as the last one
      if MODAL:
        if command == lastcommand:
          outstring.pop(0)

      # Now add the remaining parameters in order
      for param in params:
        if param in c.Parameters:
          if param == 'F':
            if command not in RAPID_MOVES:
              speed = Units.Quantity(c.Parameters['F'], FreeCAD.Units.Velocity)
              if speed.getValueAs(UNIT_SPEED_FORMAT) > 0.0:
                outstring.append(param + format(float(speed.getValueAs(UNIT_SPEED_FORMAT)), precision_string))
          elif param in ['T', 'H', 'D', 'S', 'P', 'L']:
            outstring.append(param + str(c.Parameters[param]))
          elif param in ['A', 'B', 'C']:
            outstring.append(param + format(c.Parameters[param], precision_string))
          else:  # [X, Y, Z, U, V, W, I, J, K, R, Q] (Conversion eventuelle mm/inches)
            pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length)
            outstring.append(param + format(float(pos.getValueAs(UNIT_FORMAT)), precision_string))

      # store the latest command
      lastcommand = command

      # Memorizes the current position for calculating the related movements and the withdrawal plan
      if command in MOTION_COMMANDS:
        if 'X' in c.Parameters:
          CURRENT_X = Units.Quantity(c.Parameters['X'], FreeCAD.Units.Length)
        if 'Y' in c.Parameters:
          CURRENT_Y = Units.Quantity(c.Parameters['Y'], FreeCAD.Units.Length)
        if 'Z' in c.Parameters:
          CURRENT_Z = Units.Quantity(c.Parameters['Z'], FreeCAD.Units.Length)

      if command in ('G98', 'G99'):
        DRILL_RETRACT_MODE = command

      if command in ('G90', 'G91'):
        MOTION_MODE = command


      if TRANSLATE_DRILL_CYCLES:
        if command in ('G81', 'G82', 'G83'):
          out += drill_translate(outstring, command, c.Parameters)
          # Erase the line we just translated
          outstring = []

      if SPINDLE_WAIT > 0:
        if command in ('M3', 'M03', 'M4', 'M04'):
          out += linenumber() + format_outstring(outstring) + "\n"
          out += linenumber() + format_outstring(['G4', 'P%s' % SPINDLE_WAIT]) + "\n"
          outstring = []

      # Check for Tool Change:
      if command in ('M6', 'M06'):
        if OUTPUT_COMMENTS:
          out += linenumber() + "(Begin toolchange)\n"
        if not OUTPUT_TOOL_CHANGE:
          outstring.insert(0, "(" )
          outstring.append( ")" )
        else:
          for line in TOOL_CHANGE.splitlines(True):
            out += linenumber() + line

      if command == "message":
        if OUTPUT_COMMENTS is False:
          out = []
        else:
          outstring.pop(0)  # remove the command

      if command in SUPPRESS_COMMANDS:
        outstring.insert(0, "(" )
        outstring.append( ")" )

      # prepend a line number and append a newline
      if len(outstring) >= 1:
          out += linenumber() + format_outstring(outstring) + "\n"

  return out


def drill_translate(outstring, cmd, params):
  global DRILL_RETRACT_MODE
  global MOTION_MODE
  global CURRENT_X
  global CURRENT_Y
  global CURRENT_Z
  global UNITS
  global UNIT_FORMAT
  global UNIT_SPEED_FORMAT

  strFormat = '.' + str(PRECISION) + 'f'

  trBuff = ""

  if OUTPUT_COMMENTS:  # Comment the original command
    outstring[0] = "(" + outstring[0]
    outstring[-1] = outstring[-1] + ")"
    trBuff += linenumber() + format_outstring(outstring) + "\n"

  # cycle conversion
  # currently only cycles in XY are provided (G17)
  # other plains ZX (G18) and  YZ (G19) are not dealt with : Z drilling only.
  drill_X = Units.Quantity(params['X'], FreeCAD.Units.Length)
  drill_Y = Units.Quantity(params['Y'], FreeCAD.Units.Length)
  drill_Z = Units.Quantity(params['Z'], FreeCAD.Units.Length)
  RETRACT_Z = Units.Quantity(params['R'], FreeCAD.Units.Length)
  # R less than Z is error
  if RETRACT_Z < drill_Z :
    trBuff += linenumber() + "(drill cycle error: R less than Z )\n"
    return trBuff

  if MOTION_MODE == 'G91':   # G91 relative movements
    drill_X += CURRENT_X
    drill_Y += CURRENT_Y
    drill_Z += CURRENT_Z
    RETRACT_Z += CURRENT_Z

  if DRILL_RETRACT_MODE == 'G98' and CURRENT_Z >= RETRACT_Z:
    RETRACT_Z = CURRENT_Z

  # get the other parameters
  drill_feedrate = Units.Quantity(params['F'], FreeCAD.Units.Velocity)
  if cmd == 'G83':
    drill_Step = Units.Quantity(params['Q'], FreeCAD.Units.Length)
    a_bit = drill_Step  * 0.05    # NIST 3.5.16.4 G83 Cycle:  "current hole bottom, backed off a bit."
  elif cmd == 'G82':
    drill_DwellTime = params['P']

  # wrap this block to ensure machine MOTION_MODE is restored in case of error
  try:
    if MOTION_MODE == 'G91':
      trBuff += linenumber() + "G90\n"  # force absolute coordinates during cycles

    strG0_RETRACT_Z = 'G0 Z' + format(float(RETRACT_Z.getValueAs(UNIT_FORMAT)), strFormat) + "\n"
    strF_Feedrate = ' F' + format(float(drill_feedrate.getValueAs(UNIT_SPEED_FORMAT)), '.2f') + "\n"
    print (strF_Feedrate)

    # preliminary mouvement(s)
    if CURRENT_Z < RETRACT_Z:
      trBuff += linenumber() + strG0_RETRACT_Z
    trBuff += linenumber() + 'G0 X' + format(float(drill_X.getValueAs(UNIT_FORMAT)), strFormat) + ' Y' + format(float(drill_Y.getValueAs(UNIT_FORMAT)), strFormat) + "\n"
    if CURRENT_Z > RETRACT_Z:
      # NIST GCODE 3.5.16.1 Preliminary and In-Between Motion says G0 to RETRACT_Z. Here use G1 since retract height may be below surface !
      trBuff += linenumber() + 'G1 Z' + format(float(RETRACT_Z.getValueAs(UNIT_FORMAT)), strFormat) + strF_Feedrate
    last_Stop_Z = RETRACT_Z

    # drill moves
    if cmd in ('G81', 'G82'):
      trBuff += linenumber() + 'G1 Z' + format(float(drill_Z.getValueAs(UNIT_FORMAT)), strFormat) + strF_Feedrate
      # pause where applicable
      if cmd == 'G82':
        trBuff += linenumber() + 'G4 P' + str(drill_DwellTime) + "\n"
      trBuff += linenumber() + strG0_RETRACT_Z
    else:  # 'G83'
      if params['Q'] != 0 :
        while 1:
          if last_Stop_Z != RETRACT_Z :
            clearance_depth = last_Stop_Z + a_bit  # rapid move to just short of last drilling depth
            trBuff += linenumber() + 'G0 Z' + format(float(clearance_depth.getValueAs(UNIT_FORMAT)) , strFormat) + "\n"
          next_Stop_Z = last_Stop_Z - drill_Step
          if next_Stop_Z > drill_Z:
            trBuff += linenumber() + 'G1 Z' + format(float(next_Stop_Z.getValueAs(UNIT_FORMAT)), strFormat) + strF_Feedrate
            trBuff += linenumber() + strG0_RETRACT_Z
            last_Stop_Z = next_Stop_Z
          else:
            trBuff += linenumber() + 'G1 Z' + format(float(drill_Z.getValueAs(UNIT_FORMAT)), strFormat) + strF_Feedrate
            trBuff += linenumber() + strG0_RETRACT_Z
            break

  except Exception as e:
    pass

  if MOTION_MODE == 'G91':
    trBuff += linenumber() + 'G91'  # Restore if changed

  return trBuff


# print(__name__ + ": GCode postprocessor loaded.")

Greetings,
Philipp
mea08kw
Posts: 82
Joined: Sun Oct 09, 2022 6:22 am

Re: I need help for scripting 3D laser engraving paths

Post by mea08kw »

PhilippDahlem wrote: Fri Jan 07, 2022 2:55 pm
I came across another weird thing in FreeCAD, which is a bit offtopic. There is noch function to boxselect edges. It just works for faces. I think i will open another discussion for that, since i think, boxselection of solids, faces, edges or vertices would be a sensefull basic function.
I do agree that the optimization of box selection for vertex, even control points in Bezier surface would be useful.

Hopefully we have contributors in this part.
Post Reply