Source
fill.py
"""Scanline (tatami) fill plus edge running-stitch for letter polygons."""
import math
from shapely.geometry import LineString, MultiPolygon, Polygon
from .params import StitchParams
Stitch = tuple[float, float, bool] # (x_mm, y_mm, jump)
def stitch_letters(letters: list[MultiPolygon],
params: StitchParams | None = None) -> list[Stitch]:
p = params or StitchParams()
out: list[Stitch] = []
for geom in letters:
for poly in geom.geoms:
stitch_polygon(poly, out, p)
return out
def stitch_polygon(poly: Polygon, out: list[Stitch], p: StitchParams) -> None:
"""Tatami fill + running-stitch outline for one polygon."""
_fill(poly, out, p)
_outline(poly, out, p)
def _fill(poly: Polygon, out: list[Stitch], p: StitchParams) -> None:
safe = poly.buffer(0.05) # tolerance for boundary-hugging travel checks
for i, segs in enumerate(_rows(poly, p)):
if i % 2: # serpentine: alternate rows run right-to-left
segs = [(e, s) for s, e in reversed(segs)]
for start, end in segs:
travel_to(safe, out, start, p)
_run(out, start, end, p)
def _rows(poly: Polygon, p: StitchParams) -> list[list[tuple]]:
minx, miny, maxx, maxy = poly.bounds
rows = []
y = miny + p.row_spacing_mm / 2
while y < maxy:
cut = poly.intersection(LineString([(minx - 1, y), (maxx + 1, y)]))
segs = []
for g in getattr(cut, "geoms", [cut]):
if isinstance(g, LineString) and g.length >= p.min_row_segment_mm:
x1, x2 = g.coords[0][0], g.coords[-1][0]
segs.append(((min(x1, x2), y), (max(x1, x2), y)))
if segs:
rows.append(sorted(segs))
y += p.row_spacing_mm
return rows
def travel_to(safe: Polygon, out: list[Stitch], point: tuple,
p: StitchParams) -> None:
if out:
lx, ly, _ = out[-1]
if (lx, ly) == point:
return
hop = LineString([(lx, ly), point])
if hop.length <= p.connect_max_mm and safe.covers(hop):
out.append((point[0], point[1], False)) # short in-shape travel: sew it
return
out.append((point[0], point[1], True)) # otherwise jump
def _run(out: list[Stitch], start: tuple, end: tuple, p: StitchParams) -> None:
n = max(1, math.ceil(math.dist(start, end) / p.max_stitch_mm))
for k in range(1, n + 1):
t = k / n
out.append((start[0] + (end[0] - start[0]) * t,
start[1] + (end[1] - start[1]) * t, False))
def _outline(poly: Polygon, out: list[Stitch], p: StitchParams) -> None:
safe = poly.buffer(0.05)
for ring in [poly.exterior, *poly.interiors]:
n = max(3, math.ceil(ring.length / p.outline_stitch_mm))
pts = [ring.interpolate(ring.length * k / n) for k in range(n + 1)]
travel_to(safe, out, (pts[0].x, pts[0].y), p)
out.extend((pt.x, pt.y, False) for pt in pts[1:])