imrishabh18/rp2040-motor-controller
An RP2040-based NEMA17 stepper-motor controller with USB-C programming and PD motor-power negotiation, DRV8825 dual H-bridge drive, current/temperature telemetry, protected power filtering, status RGB LED, buzzer alarm, and optional magnetic encoder.
- Version
- 1.0.41
- License
- unset
- Stars
- 3
scripts/gerber_geometry.py
"""Read the subset emitted by the pinned Gerber exporter, rejecting unknown commands."""
import re
from shapely.geometry import Point, LineString, Polygon, box
from shapely.affinity import translate
from shapely.ops import unary_union
from shapely import make_valid
def gerber_geometry(text):
assert '%FSLAX46Y46*%' in text and '%MOMM*%' in text
apertures = {}
for number, kind, args in re.findall(r'%ADD(\d+)([^,]+),([^*]+)\*%', text):
p = list(map(float, args.split('X')))
if kind == 'C':
shape = Point(0, 0).buffer(p[0] / 2, quad_segs=64)
elif kind == 'R':
shape = box(-p[0]/2, -p[1]/2, p[0]/2, p[1]/2)
elif kind in ('HORZPILL', 'VERTPILL'):
w, h = p[:2]; r = min(w, h) / 2
shape = LineString([(-w/2+r, -h/2+r), (w/2-r, h/2-r)]).buffer(r, quad_segs=64)
elif kind == 'ROUNDRECT':
# Exporter macro: centre polygon + four radius-r corner circles
# + width-2r edge rectangles (including already-rotated corners).
assert len(p) == 9 and p[0] > 0
macro = re.search(r'%AMROUNDRECT\*(.*?)%', text, re.S)
assert macro is not None
operations = [line.strip() for line in macro[1].split('*')
if line.strip() and not line.strip().startswith('0 ')]
assert operations == [
'4,1,4,$2,$3,$4,$5,$6,$7,$8,$9,$2,$3,0',
'1,1,$1+$1,$2,$3', '1,1,$1+$1,$4,$5',
'1,1,$1+$1,$6,$7', '1,1,$1+$1,$8,$9',
'20,1,$1+$1,$2,$3,$4,$5,0', '20,1,$1+$1,$4,$5,$6,$7,0',
'20,1,$1+$1,$6,$7,$8,$9,0', '20,1,$1+$1,$8,$9,$2,$3,0',
], 'Unknown rounded rectangle macro'
shape = Polygon(list(zip(p[1::2], p[2::2]))).buffer(p[0], quad_segs=64)
else:
raise AssertionError(f'Unsupported Gerber aperture {kind}')
apertures[int(number)] = (kind, p, shape)
# Apply dark/clear exposures in file order, including pour cutouts.
text = re.sub(r'%LP([DC])\*%', r'LP\1*', text)
commands = re.sub(r'%.*?%', '', text, flags=re.S).split('*')
shapes = []; aperture = None; last = None; region = None
copper = Polygon(); polarity = 'D'
def expose():
nonlocal copper, shapes
if shapes:
batch = unary_union([make_valid(shape) for shape in shapes])
copper = copper.union(batch) if polarity == 'D' else copper.difference(batch)
shapes = []
for command in map(str.strip, commands):
if not command or command.startswith('G04') or command in ('G01', 'G75', 'M02'):
continue
if command in ('LPD', 'LPC'):
expose(); polarity = command[-1]; continue
if command == 'G36':
assert region is None; region = []; continue
if command == 'G37':
assert region is not None and len(region) >= 3
shapes.append(Polygon(region)); region = None; continue
if re.fullmatch(r'D\d+', command):
aperture = apertures[int(command[1:])]; continue
m = re.fullmatch(r'X(-?\d+)Y(-?\d+)D0([123])', command)
assert m, f'Unsupported Gerber command {command}'
point = (int(m[1]) / 1e6, int(m[2]) / 1e6); op = int(m[3])
if region is not None:
assert op in (1, 2); region.append(point)
elif op == 3:
shapes.append(translate(aperture[2], *point))
elif op == 1:
assert last is not None and aperture[0] == 'C'
shapes.append(LineString([last, point]).buffer(aperture[1][0] / 2, quad_segs=64))
last = point
assert region is None
expose()
return copper
