#!/usr/bin/python from __future__ import print_function import os, sys, re, getopt, numpy as np, math, json from signal import signal, SIGPIPE, SIG_DFL signal(SIGPIPE,SIG_DFL) VERSION = "0.1.0" ctx = { "units" : "mm", "epsilon" : 1.0/1024.0, "sort": False, "explicit_speed": False, "premul" : 1.0, "premul_x" : 1.0, "premul_y" : 1.0, "premul_z" : 1.0, "header" : "", "footer" : "", "g0speed" : "", "g1speed" : "", "z_active": False, "z_step" : 7.0, "z_height" : 5.0, "z_plunge" : -21.0, "z_0" : 0.0, "z_slow" : "", "z_rapid" : "", "tab_n" : 0, "tab_offset" : 0.0, "tab_length" : 50.0, "tab_height" : 5.0, "tab_slide_factor" : 1/8.0, "tab_default_n" : 4, "close_polygon": True } ctx_laser = { "units" : "mm", "epsilon" : 1.0/1024.0, "sort": False, "explicit_speed": False, "premul" : 1.0, "premul_x" : 1.0, "premul_y" : 1.0, "premul_z" : 1.0, "header" : "", "footer" : "", "g0speed" : "F5000", "g1speed" : "F3000", "z_active": False, "z_step" : 0.0, "z_height" : 0.0, "z_plunge" : 0.0, "z_0" : 0.0, "z_slow" : 0.0, "z_rapid" : 0.0, "tab_n" : 0, "tab_offset" : 0.0, "tab_length" : 2.5, "tab_height" : 0.0, "tab_slide_factor" : 1/8.0, "tab_default_n" : 4, "close_polygon": True } ctx_maslow = { "units" : "mm", "epsilon" : 1.0/(1024.0*1024.0), "sort": False, "explicit_speed": False, "premul" : 1.0, "premul_x" : 1.0, "premul_y" : 1.0, "premul_z" : 1.0, "header" : "", "footer" : "", #"g0speed" : 800.0, "g1speed" : 800.0, "g0speed" : "", "g1speed" : "", "z_active": True, "z_step" : 7.0, "z_height" : 5.0, "z_plunge" : -21.0, "z_0" : 0.0, "z_slow" : "", "z_rapid" : "", #"tab_n" : 3, "tab_offset" : 0.0, "tab_length" : 50.0, "tab_height" : 3.0, "tab_slide_factor" : 1/8.0, "tab_n" : 3, "tab_offset" : 0.0, "tab_length" : 50.0, "tab_height" : 10.0, "tab_slide_factor" : 1/8.0, "tab_default_n" : 3, "close_polygon": True } def usage(): print("version:", VERSION) print("usage:") print("") print(" gp2ngc [options] ") print("") print(" [--preset preset] use preset tool context (options are 'maslow', 'laser')") print(" [-i infile] input file ('-' for stdin, default)") print(" [-o outfile] output file ('-' for stdout, default)") print(" [--header str] string to print before any processing") print(" [--footer str] string to print after any processing") print(" [--rapid str] string to append at end of rapid motion (G0)") print(" [--slow str] string to append at end of slow motion (G1)") print(" [--z-rapid str] z rapid (default to rapid)") print(" [--z-slow str] z rapid (default to rapid)") print(" [--z-step dz] z step down per pass") print(" [--z-raise Z] z height to raise z to for rapid motion") print(" [--z-plunge z] z final plunge depth") print(" [--tab-n n] insert n tabs per contour") print(" [--tab-length s] tab length") print(" [--tab-height h] tab height") print(" [--show-context] show context information") print(" [--close-polygon] connect first and last point in polygon point list (default)") print(" [--open-polygon] do not connect first and last point in polygon point list") print("") def print_polygon_debug(pgn, ofp=sys.stdout): print("", file=ofp) for idx in range(len(pgn)): print("#", idx, pgn[idx]["t"], ":", pgn[idx]["x"], pgn[idx]["y"], pgn[idx]["s"], "n:", pgn[idx]["n"], file=ofp) def print_polygon_debug2(pgn, ofp=sys.stdout): print("", file=ofp) for idx in range(len(pgn)): z = 0.0 if pgn[idx]["t"] == "t": z = 1 print(pgn[idx]["x"], pgn[idx]["y"], z, file=ofp) #if idx>0 and ((pgn[idx-1]["t"] == "." and pgn[idx]["t"] == "t") or (pgn[idx-1]["t"] == "t" and pgn[idx]["t"] == ".")): # print( "\n", file=ofp) def cmp_pgn_ele(a,b): if (len(a) == 0): return -1 if (len(b) == 0): return 1 s0 = math.sqrt(a[0][0]*a[0][0] + a[0][1]*a[0][1]) s1 = math.sqrt(b[0][0]*b[0][0] + b[0][1]*b[0][1]) if s0 < s1: return -1 if s1 > s0: return 1 return 0 def polygons_sort(pgns): pgns.sort(cmp_pgn_ele) def print_polygon(pgn, ofp=sys.stdout): print("", file=ofp) for idx in range(len(pgn)): print(pgn[idx]["x"], pgn[idx]["y"], file=ofp) def crossprodmag(u,v, eps=1.0/1024.0): ux = u["x"] uy = u["y"] us = math.sqrt(ux*ux + uy*uy) vx = v["x"] vy = v["y"] vs = math.sqrt(vx*vx + vy*vy) if (abs(us) < eps) or (abs(vs) < eps): return 0.0 return ((ux*vy) - (uy*vx))/(us*vs) def ingest_egest_orig(ctx, ifp = sys.stdin, ofp = sys.stdout): polygons = [] polygon = [] print(ctx["header"], file=ofp) line_no=0 #for line in sys.stdin: for line in ifp: line = line.strip() line_no += 1 if (len(line)==0) or (line == ""): if len(polygon)>0: polygons.append(polygon) polygon = [] continue if line[0]=='#': continue r = re.split(r'\s+', line) if len(r)!=2: print("Error on line " + str(line_no) + ": number of arguments is not 2 (" + line + ")", file=sys.stderr) sys.exit(1) try: x = float(r[0]) * ctx["premul"] y = float(r[1]) * ctx["premul"] except Exception, e: print(e, file=sys.stderr) sys.exit(1) polygon.append([x,y]) for p in polygons: if len(p)==0: continue x0 = p[0][0] y0 = p[0][1] print("G0", "X" + "{:.10f}".format(x0), "Y" + "{:.10f}".format(y0), sfx_rapid, file=ofp) for xy in p: x = xy[0] y = xy[1] print("G1", "X" + "{:.10f}".format(x), "Y" + "{:.10f}".format(y), ctx["g1speed"], file=ofp) if ctx["close_polygon"]: print("G1", "X" + "{:.10f}".format(x0), "Y" + "{:.10f}".format(y0), ctx["g1speed"], file=ofp) print(file=ofp) print(sfx, file=ofp) def ingest_egest(ctx, ifp = sys.stdin, ofp = sys.stdout): polygons = [] polygon = [] print(ctx["header"], file=ofp) line_no=0 #for line in sys.stdin: for line in ifp: line = line.strip() line_no += 1 if (len(line)==0) or (line == ""): if len(polygon)>0: polygons.append(polygon) polygon = [] continue if line[0]=='#': continue r = re.split(r'\s+', line) if len(r)!=2: print("Error on line " + str(line_no) + ": number of arguments is not 2 (" + line + ")", file=sys.stderr) sys.exit(1) try: x = float(r[0]) * ctx["premul"] y = float(r[1]) * ctx["premul"] except Exception, e: print(e, file=sys.stderr) sys.exit(1) polygon.append([x,y]) if len(polygon)>0: polygons.append(polygon) if ctx["sort"]: polygons_sort(polygons) for p in polygons: if len(p)==0: continue x0 = p[0][0] y0 = p[0][1] print("G0", "X" + "{:.10f}".format(x0), "Y" + "{:.10f}".format(y0), ctx["g0speed"], file=ofp) nstep=1 if ctx["z_active"]: print("G0", "Z" + "{:.10f}".format(ctx["z_height"]), ctx["z_rapid"], file=ofp) nstep = int(math.ceil(( abs(ctx["z_plunge"] - ctx["z_0"]) )/ctx["z_step"])) for s in range(nstep): if ctx["z_active"]: zh = ((ctx["z_plunge"] - ctx["z_0"]) * float(s+1)/float(nstep)) + ctx["z_0"] if zh < ctx["z_plunge"]: print(";#(CLAMPING)", file=ofp) zh = ctx["z_plunge"] print("G1", "Z" + "{:.10f}".format(zh), ctx["z_slow"], file=ofp) for xy in p: x = xy[0] y = xy[1] print("G1", "X" + "{:.10f}".format(x), "Y" + "{:.10f}".format(y), ctx["g1speed"], file=ofp) if ctx["close_polygon"]: print("G1", "X" + "{:.10f}".format(x0), "Y" + "{:.10f}".format(y0), ctx["g1speed"], file=ofp) print(file=ofp) if ctx["z_active"]: print("G0", "Z" + "{:.10f}".format(ctx["z_height"]), ctx["z_rapid"], file=ofp) print(ctx["footer"], file=ofp) ## decorate the pgn array of objects with the cross product normal ## magnitude value 'n' ## def polygon_decorate_n(pgn): if len(pgn) < 3: for idx in range(len(pgn)): pgn[idx]["n"] = 0.0 return for idx in range(len(pgn)): prv_idx = (idx + len(pgn) - 1) % len(pgn) nxt_idx = (idx + 1) % len(pgn) v0 = { "x": pgn[prv_idx]["x"] - pgn[idx]["x"], "y" : pgn[prv_idx]["y"] - pgn[idx]["y"] } v1 = { "x": pgn[nxt_idx]["x"] - pgn[idx]["x"], "y" : pgn[nxt_idx]["y"] - pgn[idx]["y"] } pgn[idx]["n"] = crossprodmag(v0,v1) ## calculate the 'score' of the tab placed at path s-position 'tab_beg' with ## tab length 'tab_len'. ## This is length of each portion the tab falls on the line segment times the ## magnitude of the normal. ## Since the left and right segment use the same normal, and the normal is thus ## multiplied by itself, this score is strictly positive. ## def polygon_curve_score(pgn, tab_beg, tab_len): score = 0.0 if len(pgn) < 3: return score for idx in range(len(pgn)): prv_idx = (idx + len(pgn) - 1) % len(pgn) nxt_idx = (idx + 1) % len(pgn) if (tab_beg + tab_len) < pgn[idx]["s"]: return score if tab_beg > pgn[idx]["s"]: continue s0 = abs(max(pgn[prv_idx]["s"], tab_beg) - pgn[idx]["s"]) s1 = abs(pgn[idx]["s"] - min(tab_beg + tab_len, pgn[nxt_idx]["s"])) score += s0*s1*pgn[idx]["n"]*pgn[idx]["n"] return score ## insert the tab into the 'pgn' array at s-position 'tab_beg' with ## 'tab_len'. ## This does not wrap around from the end to the beginning (though this ## might be upadted in the future). ## This constructs a new array and returns it. ## def polygon_insert_tab_at(pgn, tab_beg, tab_len): ret_p = [] prv_x = 0.0 prv_y = 0.0 prv_s = 0.0 for pnt_idx in range(len(pgn)): p = pgn[pnt_idx] if pnt_idx==0: prv_x = p["x"] prv_y = p["y"] prv_s = p["s"] # take care of pathological case when line segment is 0 length # r = math.sqrt((prv_x - p["x"])*(prv_x - p["x"]) + (prv_y - p["y"])*(prv_y - p["y"])) if abs(r) < ctx["epsilon"]: prv_x = p["x"] prv_y = p["y"] prv_s = p["s"] r = 1.0 # If the tab is completel to the 'left' of the current vertex, we can insert it # completely without needing to split the tab into segements. # Do so, making sure \to insert an extra 'down' entry so that if we are right # next to another tab in the list, we'll properly render the outline. # if (tab_beg <= p["s"]) and ((tab_beg + tab_len) < p["s"]): ds = tab_beg - prv_s x = (ds * (p["x"] - prv_x) / r) + prv_x y = (ds * (p["y"] - prv_y) / r) + prv_y ret_p.append( {"x": x, "y": y, "s": tab_beg, "t": "t", "n":0.0 } ) ds = tab_beg + tab_len - prv_s x = (ds * (p["x"] - prv_x) / r) + prv_x y = (ds * (p["y"] - prv_y) / r) + prv_y ret_p.append( {"x": x, "y": y, "s": tab_beg + tab_len, "t": "t", "n":0.0 } ) ret_p.append( {"x": x, "y": y, "s": tab_beg + tab_len, "t": ".", "n":0.0 } ) ret_p += pgn[pnt_idx:] return ret_p pnt_type = p["t"] # The tab is split across the current vertex and entries to the right. # Shave off the head of the tab, add/modify the entries to create a 'tab' # entry in the 'pgn' list. # if (tab_beg <= p["s"]) and ((tab_beg + tab_len) > p["s"]): ds = tab_beg - prv_s x = (ds * (p["x"] - prv_x) / r) + prv_x y = (ds * (p["y"] - prv_y) / r) + prv_y ret_p.append( {"x": x, "y": y, "s": tab_beg, "t": "t", "n": 0.0 } ) pnt_type = "t" tab_len -= (p["s"] - tab_beg) tab_beg = p["s"] ret_p.append( { "x" : p["x"], "y": p["y"], "s": p["s"], "t": pnt_type, "n": 0.0 }) prv_x = p["x"] prv_y = p["y"] prv_s = p["s"] return ret_p ## Process polygon and insert tabs. ## def ingest_egest_with_tabs(ctx, ifp = sys.stdin, ofp = sys.stdout): polygons = [] polygon = [] print(ctx["header"], file=ofp) firstPoint = True prev_x = 0.0 prev_y = 0.0 line_no=0 for line in ifp: line = line.strip() line_no += 1 if (len(line)==0) or (line == ""): if len(polygon)>0: polygons.append(polygon) polygon = [] firstPoint = True continue if line[0]=='#': continue r = re.split(r'\s+', line) if len(r)!=2: print("Error on line " + str(line_no) + ": number of arguments is not 2 (" + line + ")", file=sys.stderr) sys.exit(1) try: x = float(r[0]) * ctx["premul"] y = float(r[1]) * ctx["premul"] except Exception, e: print(e, file=sys.stderr) sys.exit(1) # record lenght of outline as we go # s = 0.0 if not firstPoint: ds = math.sqrt( (prev_x - x)*(prev_x - x) + (prev_y - y)*(prev_y - y) ) s = polygon[ len(polygon) - 1 ]["s"] + ds prev_x = x prev_y = y # '.' type ("T" field) represent simple contour whereas 't' type # represents tabs. Though space iniefficient, it's easier # to decorate entries with modifiers like this than do it a more # complicated but efficietn way. # polygon.append({ "x":x, "y":y, "s": s, "t": ".", "n":0.0 }) firstPoint = False if len(polygon)!=0: polygons.append(polygon) if ctx["sort"]: polygons_sort(polygons) # decorate with 'n' cross product magnitude value for # score tab positioning heuristic. # for pgn_idx in range(len(polygons)): pgn = polygons[pgn_idx] if len(pgn)<3: for idx in range(len(pgn)): pgn[idx]["n"] = 0.0 continue for idx in range(len(pgn)): prv_idx = (idx + len(pgn) - 1) % len(pgn) nxt_idx = (idx + 1) % len(pgn) v0 = { "x": pgn[prv_idx]["x"] - pgn[idx]["x"], "y" : pgn[prv_idx]["y"] - pgn[idx]["y"] } v1 = { "x": pgn[nxt_idx]["x"] - pgn[idx]["x"], "y" : pgn[nxt_idx]["y"] - pgn[idx]["y"] } pgn[idx]["n"] = crossprodmag( v0, v1 ) _zheight = ctx["z_height"] _zplunge = ctx["z_plunge"] _zzero = ctx["z_0"] _zstep = ctx["z_step"] _g0speed = ctx["g0speed"] _g1speed = ctx["g1speed"] _tabheight = ctx["tab_height"] _ztabstart = _zplunge + _tabheight _tablen = ctx["tab_length"] _ntab = ctx["tab_n"] # a bit inefficient but construct tabs # for pidx in range(len(polygons)): p = polygons[pidx] if len(p)==0: continue polygon_decorate_n(polygons[pidx]) clen = p[ len(p) - 1 ]["s"] s_window_len = (clen / (float(_ntab))) - _tablen if _tablen <= 0.0: continue if clen < (float(_ntab) * _tablen): continue for tabidx in range(_ntab): tab_s_offset = float(tabidx) * clen / float(_ntab) # this heuristic, for simplicity, only uses two scores, one at the original # offset and another at some distance away that doesn't overlap with the # other tab, to determine where the tab should be positioned. # n_tab_sample = 4 score, min_score, min_idx = [], 0, 0 for idx in range(n_tab_sample): tab_shift = float(idx) * s_window_len / float(n_tab_sample) score.append( polygon_curve_score(polygons[pidx], tab_s_offset + tab_shift , _tablen) ) #print("#", tab_s_offset + tab_shift, score[idx]) if idx==0: min_score = score[0] min_idx = 0 elif min_score > score[idx]: min_score = score[idx] min_idx = idx #print("## min_score", min_score, ", min_idx", min_idx) tab_shift = float(min_idx) * s_window_len / float(n_tab_sample) polygons[pidx] = polygon_insert_tab_at(polygons[pidx], tab_s_offset + tab_shift, _tablen) #if score0 == min(score0, score1, score2): # polygons[pidx] = polygon_insert_tab_at(polygons[pidx], tab_s_offset, _tablen) #elif score1 == min(score0, score1, score2): # polygons[pidx] = polygon_insert_tab_at(polygons[pidx], tab_s_offset + s_window_len/2, _tablen) #else: # polygons[pidx] = polygon_insert_tab_at(polygons[pidx], tab_s_offset + s_window_len, _tablen) #print_polygon_debug(polygons[pidx]) for p in polygons: if len(p)==0: continue x0 = p[0]["x"] y0 = p[0]["y"] print("G0", "X" + "{:.10f}".format(x0), "Y" + "{:.10f}".format(y0), _g0speed, file=ofp) nstep=1 if ctx["z_active"]: print("G0", "Z" + "{:.10f}".format(_zheight), _g0speed, file=ofp) nstep = int(abs(math.ceil((_zplunge - _zzero)/_zstep))) prev_entry_type = "." for s in range(nstep): if ctx["z_active"]: zh = ((_zplunge - _zzero) * float(s+1)/float(nstep)) + _zzero if zh < _zplunge: zh = _zplunge #if (zh < _ztabstart) and (prev_entry_type == ".") and (xy["t"] == "t"): # #print(";# up!", file=ofp) # print("G1", "Z" + "{:.10f}".format(_ztabstart), _g1speed, file=ofp) #else: # print("G1", "Z" + "{:.10f}".format(zh), _g1speed, file=ofp) firstIter = True for xy in p: if firstIter: if (zh < _ztabstart) and (prev_entry_type == ".") and (xy["t"] == "t"): #print(";# up!", file=ofp) print("G1", "Z" + "{:.10f}".format(_ztabstart), _g1speed, file=ofp) else: print("G1", "Z" + "{:.10f}".format(zh), _g1speed, file=ofp) firstIter = False if ctx["z_active"] and (zh < _ztabstart): if (prev_entry_type == "t") and (xy["t"] == "."): #print(";# down!", file=ofp) print("G1", "Z" + "{:.10f}".format(zh), _g1speed, file=ofp) x = xy["x"] y = xy["y"] print("G1", "X" + "{:.10f}".format(x), "Y" + "{:.10f}".format(y), _g1speed, file=ofp) if ctx["z_active"]: if zh < _ztabstart: if (prev_entry_type == ".") and (xy["t"] == "t"): #print(";# up!", file=ofp) print("G1", "Z" + "{:.10f}".format(_ztabstart), _g1speed, file=ofp) prev_entry_type = xy["t"] #if ctx["z_active"]: if ctx["close_polygon"]: print("G1", "X" + "{:.10f}".format(x0), "Y" + "{:.10f}".format(y0), _g1speed, file=ofp) print(file=ofp) print("G0", "Z" + "{:.10f}".format(_zheight), _g0speed, file=ofp) print(ctx["footer"], file=ofp) def main(argv): global ctx ifn, ofn = "-", "-" ifp, ofp = sys.stdin, sys.stdout long_opt_list = [ "help", "preset=", "show-context", "z", "explicit-speed", "premul=", "epsilon=", "header=", "footer=", "rapid=", "slow=", "z-rapid=", "z-slow=", "z-step=", "z-raise=", "z-plunge=", "tab", "tab-n=", "tab-length=", "tab-height=", "notab", "close-polygon", "open-polygon", "sort"] try: #opts,args = getopt.getopt(argv,"hi:o:",["sfx-final=", "pfx=", "sfx-rapid=","sfx-slow=", "premul=", "z-step", "z-raise", "z-plunge"]) opts,args = getopt.getopt(argv,"hi:o:",long_opt_list) except getopt.GetoptError: usage() sys.exit(2) if len(args) >= 1: ifn = args[0] if len(args) >= 2: ofn = args[1] for opt, arg in opts: if opt == "--preset": if arg.lower() in ("maslow", "maslowcnc"): ctx = ctx_maslow elif arg.lower() in ("laser"): ctx = ctx_laser else: print("WARNING: no preset found, using default", file=sys.stderr) show_context = False for opt, arg in opts: if opt in ("-h", "--help"): usage() sys.exit() elif opt in ("--show-context"): show_context = True elif opt in ("-i", "--ifile"): ifn = arg elif opt in ("-o", "--ofile"): ofn = arg elif opt in ("--premul"): ctx["premul"] = float(arg) elif opt in ("--rapid"): ctx["g0speed"] = arg ctx["explicit_speed"] = True elif opt in ("--slow"): ctx["g1speed"] = arg ctx["explicit_speed"] = True elif opt in ("--explicit-speed"): ctx["explicit_speed"] = True elif opt in ("--header"): ctx["header"] = arg.decode('unicode_escape') elif opt in ("--footer"): ctx["footer"] = arg.decode('unicode_escape') elif opt in ("--close-polygon"): ctx["close_polygon"] = True elif opt in ("--open-polygon"): ctx["close_polygon"] = False elif opt in ("--z"): ctx["z_active"] = True elif opt in ("--z-slow"): ctx["z_slow"] = float(arg) elif opt in ("--z-rapid"): ctx["z_rapid"] = float(arg) elif opt in ("--z-step"): ctx["z_step"] = float(arg) elif opt in ("--z-raise"): ctx["z_height"] = float(arg) elif opt in ("--z-plunge"): ctx["z_active"] = True ctx["z_plunge"] = float(arg) elif opt in ("--notab"): ctx["tab_n"] = 0 elif opt in ("--tab"): ctx["tab_n"] = ctx["tab_default_n"] elif opt in ("--tab-n"): ctx["tab_n"] = int(arg) elif opt in ("--tab-length"): ctx["tab_length"] = float(arg) elif opt in ("--tab-height"): ctx["tab_height"] = float(arg) elif opt in ("--sort"): ctx["sort"] = True if show_context: print(json.dumps(ctx, indent=2)) sys.exit(0) if ifn != "-": ifp = open(ifn,"r") if ofn != "-": ofp = open(ofn, "w") if ctx["tab_n"] > 0 : ingest_egest_with_tabs(ctx, ifp, ofp) else: ingest_egest(ctx, ifp, ofp) if ifp!=sys.stdin: ifp.close() if ofp!=sys.stdout: ofp.close() if __name__ == "__main__": main(sys.argv[1:])