Click here to surface your spoil board with this one easy Python Script

OK, I don’t want to do that conversationally again, so I certainly want a generator. It’s probably quicker to write one than to search for one that works the way I want.

$ ./generate-surface.py --help
usage: generate-surface.py [-h] --x-max X_MAX --y-max Y_MAX --pitch PITCH --feed
                           FEED [--z-depth Z_DEPTH] [--safe SAFE] [--only-x]
                           [--only-y] [--boustrophedonically] [--monotonically]

Generate gcode for spoilboard surfacing.

options:
  -h, --help            show this help message and exit
  --x-max X_MAX, -x X_MAX
                        X working width
  --y-max Y_MAX, -y Y_MAX
                        Y working width
  --pitch PITCH, -p PITCH
                        Pitch of passes/step over
  --feed FEED, -f FEED  Horizontal feed
  --z-depth Z_DEPTH, -z Z_DEPTH
                        Z depth to cut (0)
  --safe SAFE, -s SAFE  Safe Z height (3)
  --only-x, -w          Surface only in X (Default both)
  --only-y, -d          Surface only in Y (Default both)
  --boustrophedonically, -b
                        Run boustrophodonically instead of default monotonically
  --monotonically, -m   Run monotonically (default)
#!/usr/bin/python

import argparse

parser = argparse.ArgumentParser(description='Generate gcode for spoilboard surfacing.')
parser.add_argument('--x-max', '-x', type=int, required=True, help='X working width')
parser.add_argument('--y-max', '-y', type=int, required=True, help='Y working width')
parser.add_argument('--pitch', '-p', type=int, required=True, help='Pitch of passes/step over')
parser.add_argument('--feed', '-f', type=int, required=True, help='Horizontal feed')
parser.add_argument('--z-depth', '-z', type=float, default=0, help='Z depth to cut (0)')
parser.add_argument('--safe', '-s', type=float, default=3, help='Safe Z height (3)')
parser.add_argument('--only-x', '-w', default=False, action='store_true', help='Surface only in X (Default both)')
parser.add_argument('--only-y', '-d', default=False, action='store_true', help='Surface only in Y (Default both)')
parser.add_argument('--boustrophedonically', '-b', default=False, action='store_true', help='Run boustrophodonically instead of default monotonically')
parser.add_argument('--monotonically', '-m', dest='boustrophedonically', action='store_false', help='Run monotonically (default)')
args = parser.parse_args()
v = vars(args)

step_pitch = {False: args.pitch, True: args.pitch*2}[args.boustrophedonically]
def steps(end):
    s = [x for x in range(0, end, step_pitch)]
    if s[-1] != end:
        # end not a multiple of pitch
        s.append(end)
    return s

x_steps = steps(args.x_max)
y_steps = steps(args.y_max)

print('G0 Z{safe}'.format(**v))
print('G0 X0 Y0')
print('G1 Z{z_depth} X{pitch} F{feed}'.format(**v))
print('G1 Z{z_depth} X0'.format(**v))

if not args.only_y:
    for y in y_steps:
        print('G1 Y{}'.format(y))
        print('G1 X{x_max}'.format(**v))
        if args.boustrophedonically:
            print('G1 Y{}'.format(y+args.pitch))
            print('G1 X0')
        else:
            print('G0 X0')
    print('G0 Y0')

if not args.only_x:
    if not args.only_y:
        steps = x_steps[1:] # X0 step already effectively run
    else:
        steps = x_steps
    for x in steps:
        print('G1 X{}'.format(x))
        print('G1 Y{y_max}'.format(**v))
        if args.boustrophedonically:
            print('G1 X{}'.format(x+args.pitch))
            print('G1 Y0')
        else:
            print('G0 Y0')

print('G0 Z{safe} X0 Y0'.format(**v))

This works only if you have Python installed.

This intentionally does rapids over already-cut surface. You can change a few G0 to G1 if you don’t like that and it will then honor the feed rate for everything. I didn’t add a vertical feed rate because the ease-down should be dominated by the pitch anyway, so plunge rate isn’t a concern. I think.

Update from the original version: While the default is still monotonic, I added a boustrophedonic option and an option to run only X-major (side to side) and Y-major (front to back) for quicker cleanup surfacing. I even use that with a 50mm or so pitch to run the vacuum across the surface for quick cleanup. :smiling_face:

Update: Now prints useful message if invoked without arguments.

2 Likes

Pretty cool but had you tried making a parametric plane/3D model in CAD and feeding the 3D model to Kiri:Moto to generate the toolpath which would result in surfacing the spoil board?

I’m a fan of Kiri:Moto but that seems like a lot of extra work to me.

1 Like

Setting up Kiri or setting up a parametric model of the plane in OpenSCAD( or FreeCAD)?
I’m not great at coding anymore since I don’t do much but throwing together something for OpenSCAD Customizer is pretty quick for me.

I can see the interest in doing what you did in Python though and especially with good coding skills and an understanding in GCode. I was just thinking about how someone without programming skills might approach this since many will ask what Python is and where’s the exe file to run this. That’s where my brain went into using a 3D CAD tool to make a flat plane the size needed(square or rounded corners) and feeding it to a CAM tool to generate the GCode since they should already have some familiarity with their CAM tool(s).

I looked for help by running it without any parameters and it choked… such a nice simple app I see what you’re talking about and how Python allows you to define parameters is fantastic. So I figured out what a NoneType is and added this to my version right before the first use of the pitch variable because I couldn’t figure out how to trigger the full --help listing.

if not isinstance(args.pitch, int):
print(‘error no parameters, try using --help’)
exit()

Oh, I just forgot to mark the required options as required=True — thanks for the report, I’ll fix that.

Now if you run the latest version without necessary arguments, it will give you a more helpful message. I also made two arguments have default values instead of being required.

1 Like