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