-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathgCode.py
More file actions
138 lines (123 loc) · 5.66 KB
/
gCode.py
File metadata and controls
138 lines (123 loc) · 5.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import pandas as pd
from pathlib import Path
import argparse
import sys
def read_coords(df, xcol, ycol, idcol):
if xcol is None or ycol is None:
numeric_cols = [c for c in df.columns if c.lower() not in ("id", "group", "group_id")]
xcol = xcol or numeric_cols[0]
ycol = ycol or numeric_cols[1]
paths = []
ids = []
if idcol and idcol in df.columns:
codes, uniques = pd.factorize(df[idcol])
for code in range(len(uniques)):
rows = df.iloc[codes == code][[xcol, ycol]].dropna(how="any")
if rows.empty:
continue
path = []
for xv, yv in zip(rows[xcol], rows[ycol]):
try:
path.append((float(xv), float(yv)))
except Exception:
continue
if path:
paths.append(path)
ids.append(uniques[code])
else:
current = []
for xv, yv in zip(df[xcol], df[ycol]):
if pd.isna(xv) or pd.isna(yv):
if current:
paths.append(current)
ids.append(None)
current = []
continue
try:
current.append((float(xv), float(yv)))
except Exception:
continue
if current:
paths.append(current)
ids.append(None)
return paths, ids
def path_to_gcode(fname, paths, ids=None, z_safe=100.0, z_cut=50.0, feed_xy=1500, feed_z=3000, travel_feed=5000, scale=1.0, offset_x=0.0, offset_y=0.0):
"""
Convert paths to G-code.
Args:
fname: Output filename
paths: List of paths (each path is a list of (x, y) tuples)
ids: Optional list of polygon IDs
z_safe: Safe Z height in mm
z_cut: Cutting Z depth in mm
feed_xy: XY feed rate in mm/min
feed_z: Z feed rate in mm/min
travel_feed: Rapid travel feed rate in mm/min
scale: Scale factor to convert coordinates to mm (e.g., 0.2 for 500px -> 100mm)
offset_x: X offset in mm to center the drawing
offset_y: Y offset in mm to center the drawing
"""
lines = [
"(generated by gCode.py)",
"G90", # absolute positioning (coordinates in meters by default for Arduino)
f"(z_safe={z_safe}, z_cut={z_cut})",
f"(scale={scale}, offset_x={offset_x}, offset_y={offset_y})",
f"G0 Z{z_safe/1000:.6f} F{travel_feed}" # Convert mm to meters
]
for i, path in enumerate(paths):
if not path:
continue
# ensure lifted before traveling to new polygon start
lines.append(f"(--- polygon {i} start ---)")
lines.append(f"G0 Z{z_safe/1000:.6f} F{travel_feed} ; lift before travel")
x0, y0 = path[0]
x0_mm = x0 * scale + offset_x
y0_mm = y0 * scale + offset_y
# Convert mm to meters
x0_m = x0_mm / 1000
y0_m = y0_mm / 1000
lines.append(f"G0 X{x0_m:.6f} Y{y0_m:.6f} F{travel_feed} ; travel to polygon start")
if ids and i < len(ids) and ids[i] is not None:
lines.append(f"(Polygon ID: {ids[i]})")
lines.append(f"G1 Z{z_cut/1000:.6f} F{feed_z} ; plunge to cut depth")
# ADD: explicitly touch down at the start point as a cutting move so visualizer has the start vertex
lines.append(f"G1 X{x0_m:.6f} Y{y0_m:.6f} F{feed_xy} ; start cut at first vertex")
# cutting moves (skip duplicating first point now, so start from index 1)
for x, y in path[1:]:
x_mm = x * scale + offset_x
y_mm = y * scale + offset_y
# Convert mm to meters
x_m = x_mm / 1000
y_m = y_mm / 1000
lines.append(f"G1 X{x_m:.6f} Y{y_m:.6f} F{feed_xy}")
lines.append(f"G0 Z{z_safe/1000:.6f} F{travel_feed} ; retract after polygon")
lines.append(f"(--- polygon {i} end ---)")
lines.append(f"G0 Z{z_safe/1000:.6f} ; final retract")
Path(fname).write_text("\n".join(lines) + "\n")
return fname
if __name__ == "__main__":
p = argparse.ArgumentParser(description="Convert CSV xy coords to simple G-code")
p.add_argument("csv", nargs="?", default=None, help="Input CSV file (default: polygon_coordinates.csv next to this script)")
p.add_argument("-o", "--out", default="output.gcode", help="Output gcode filename")
p.add_argument("--xcol", default=None, help="CSV column name for X coordinates")
p.add_argument("--ycol", default=None, help="CSV column name for Y coordinates")
p.add_argument("--group-col", dest="group_col", default=None, help="Optional column that identifies each polygon/group (e.g. ID)")
p.add_argument("--z-safe", type=float, default=100.0)
p.add_argument("--z-cut", type=float, default=0.0)
p.add_argument("--feed-xy", type=float, default=1500)
p.add_argument("--feed-z", type=float, default=3000)
args = p.parse_args()
csv_path = Path(args.csv) if args.csv else Path(__file__).with_name("polygon_coordinates.csv")
if not csv_path.exists():
print(f"CSV file not found: {csv_path}", file=sys.stderr)
sys.exit(1)
df = pd.read_csv(csv_path)
# auto-detect ID column if user didn't specify
idcol = args.group_col or ("ID" if "ID" in df.columns else None)
paths, ids = read_coords(df, args.xcol, args.ycol, idcol=idcol)
if not paths:
print("No valid coordinates parsed from CSV.", file=sys.stderr)
sys.exit(1)
out = path_to_gcode(args.out, paths=paths, ids=ids, z_safe=args.z_safe, z_cut=args.z_cut, feed_xy=args.feed_xy, feed_z=args.feed_z)
total_points = sum(len(p) for p in paths)
print(f"Wrote G-code to {out} ({len(paths)} polygons, {total_points} points)")