Build Sheets

01 · Brief

Broiders is an app for managing embroidery work tied to the family business. It is meant to replace a paper receipt book and keep digitizing in house and free. It offers a simple console for managing customers, orders, and designs.

Orders is the working queue, with a customer book beside it because a lot of jobs are the same people coming back. Each order keeps its line items, due date, notes, and files, so the customer’s art and the finished stitch file live with the job. A simple accounting page counts every order toward the month it was entered and draws the goal line across the chart.

Design is one of the main reasons the app exists. Digitizing software requires sending files and money to a third party. An open source Pyembroidery library combined with Claude Code opened the door to building a personal digitizing app. With Broiders, you can type a word or drop in an SVG logo and the backend works out the real stitches, satin columns and fill rows, then writes a file the Viking will sew. The design can be saved with the customer and order for easy reuse and common patterns/settings can be saved as reusable stitch profiles.

It runs on my home server and nothing leaves the house. The whole business is one SQLite file, so a backup is a file copy.

The name is a family joke. When it is time to embroider, we ask our dog Petra, “Do you want to go do broiders?”, as if she can’t quite say embroidery. Petra knows the word now and gets excited every time it is time to “do broiders”.

02 · System

HOME SERVERDOCKERFASTAPI→ DIGITIZER
REACT UIDIGITIZER.VP3 FILEUSB STICKVIKING

03 · Source

fill.py

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
Lines 28–65 of 82 · Full source →

04 · Lessons

Lettering needs satin stitch; fill alone was not enough.

Keeping every stitch knob in one file made tuning after a test a one-file change.

05 · Revisions

VERSIONDATENOTE
Dev2026-07-04Orders, customers, Cash Money, and fill-stitch lettering.
Dev2026-07-05Login gate and Docker deploy on the home server.
Beta2026-07-22Satin-stitch lettering.
Live2026-08-25Logo mode, SVG to multi-colour stitches.