No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-08-18 22:52:57 +02:00
stitchstudio Keep resample corners and drop duplicate lock stitches so generated plans stay machine-safe. 2026-08-18 22:52:18 +02:00
tests Add GUI tests for wheel zoom cap, undo snapshot, PlanWorker cancel, and kind/pattern archive. 2026-08-18 22:52:57 +02:00
.gitignore Pin CPython 3.12 and check in a tiny sample design so clones share the same runtime and have a fixture for head-less CLI checks. 2026-08-18 21:35:21 +02:00
.pre-commit-config.yaml Switch to uv with ruff and pre-commit, add an MIT license and gitignore, and document first-clone lint commands. Originally created by Wouter Rusman. 2026-08-18 21:17:11 +02:00
.python-version Pin CPython 3.12 and check in a tiny sample design so clones share the same runtime and have a fixture for head-less CLI checks. 2026-08-18 21:35:21 +02:00
LICENSE Credit Marijn Pool as co-author alongside original author Wouter Rusman, and document pystitch-only export plus the sample fixture CLI. 2026-08-18 21:35:57 +02:00
pyproject.toml Cover the stitch-engine test plan: satin edges, params, ops, IO, and edge cases. 2026-08-18 22:52:49 +02:00
README.md Add a local pytest suite and test plan for the stitch engine. 2026-08-18 22:03:46 +02:00
run.py Apply ruff format and fix remaining lint so pre-commit can pass on Python. 2026-08-18 21:19:21 +02:00
screenshot-contour-fill.png Start commit all the files as received today 2026-08-18 20:58:37 +02:00
screenshot-fill-patterns.png Start commit all the files as received today 2026-08-18 20:58:37 +02:00
screenshot-icons.png Start commit all the files as received today 2026-08-18 20:58:37 +02:00
screenshot-layers-preview.png Start commit all the files as received today 2026-08-18 20:58:37 +02:00
uv.lock Add a local pytest suite and test plan for the stitch engine. 2026-08-18 22:03:46 +02:00

StitchStudio

Standalone, cross-platform embroidery digitising. Pictures or vectors in, machine stitch files out. No Inkscape, no extension host — one Python app.

Originally created by Wouter Rusman. Co-author Marijn Pool (marijnpool.nl).

uv sync                                          # creates .venv and installs runtime + dev tools
uv run python run.py                             # GUI
uv run python -m stitchstudio.cli logo.png -o logo.dst --width 90 --colors 4
uv run pre-commit install                        # once per clone: ruff on git commit

Development

After uv sync, run uv run pre-commit install once so git commits run the hooks. On git commit, ruff, ruff-format and a few file checks run on staged files; a failing hook blocks the commit.

To run the same tools without committing:

uv run ruff check .                              # lint
uv run ruff check --fix .                        # lint + autofix (rewrites files)
uv run ruff format .                             # formatter (rewrites files)
uv run pre-commit run --all-files                # every hook on the whole tree
uv run pre-commit run                            # staged files only
uv run pytest                                    # tests (engine + GUI smoke)

See tests/README.md for the full test plan — what to cover, why, and current status. Update that file before adding new test modules so another checkout (or LLM) can continue from the plan alone.

tests/fixtures/sample.svg is a tiny two-shape design for head-less checks:

uv run python -m stitchstudio.cli tests/fixtures/sample.svg -o /tmp/out.dst --width 40

Why these dependencies

Need Choice Why
GUI PySide6 (Qt6) LGPL, identical on Win/mac/Linux, QGraphicsView gives zoom/pan/scene units for free, QPrinter gives real printing
Geometry shapely 2 offsets, booleans, scan-line intersections; the same library Ink/Stitch leans on
Stitch files pystitch the inkstitch org's maintained Python port of libembroidery's format handlers: DST/PES/EXP/JEF/VP3/U01/XXX/PEC/SEW/PMV/TBF + SVG/PNG/G-code/PLT/QCC
SVG in svgelements resolves transforms, units, viewBox, <use>, styles — the painful 20% of SVG
Raster → vector OpenCV 5 (k-means + findContours with hole hierarchy) ships as a wheel everywhere; potrace would mean a per-platform binary

core/export.py is the only module that imports pystitch; if the package is missing the import error surfaces. BACKEND is shown in the About box and the export report. The format list is built from supported_formats() at import, so nothing needs editing when pystitch adds one.

Ink/Stitch is not usable as a library. It is an Inkscape extension: it needs inkex, an Inkscape SVG DOM and Inkscape's Python. Its ideas (tatami fill with staggered rows, satin from two rails, underlay, travel-vs-jump decisions) are what this app reimplements, and it's GPL-3, so keep that in mind if you ever copy code rather than concepts.

Architecture

stitchstudio/
  core/            no Qt anywhere — importable, scriptable, testable
    model.py       Document / Layer / Shape, params, serialisation
    ops.py         move, scale, rotate, skew, mirror, invert, boolean, grid copy, crop
    stitchgen.py   tatami fill, satin column, running stitch, underlay, lock stitches
    optimize.py    colour merge, nearest-neighbour object order, clean-up → StitchPlan
    trace.py       picture → colour layers of polygons (with holes)
    svgio.py       SVG → shapes
    export.py      StitchPlan → pystitch pattern → DST/PES/...
    project.py     .stitchproj = zip{document.json}
    threads.py     thread chart matching for the printed index
  ui/
    canvas.py      workspace: mm grid, snap, tools, selection handles, stitch preview
    rulers.py      mm rulers driven by the canvas transform
    panels.py      layers/objects tree, stitch properties, design report
    dialogs.py     trace, hoop & grid, grid copy, transforms, export
    printing.py    design at true size + thread colour index in sewing order
    mainwindow.py  actions, menus, toolbars, context menu, undo, file IO
  app.py / cli.py  entry points

Units are millimetres everywhere, y-down, origin at the top-left of the hoop — the same frame the stitch formats use, so nothing is converted twice.

Every command is one QAction, reused by the text menu, the tile toolbar and the right-click menu, so a shortcut can never disagree with a button.

Stitch engine notes

  • Fill direction — every fill defaults to automatic: the angle is taken from the long axis of the shape's minimum-area rotated rectangle, so stitches run along a leaf, a letter stem or a stripe instead of always going flat. Override it three ways (straight rows and radial only — contour and spiral have no single direction, and the angle box greys out with a tooltip saying so): the angle spin box, the A tool (drag a line across the shape, Shift snaps to 15°), or Stitching → Automatic fill direction to hand it back. Selected fills draw their direction on the canvas — dashed blue = automatic, solid red = manual.
  • Fill patterns — five, chosen per object under Fill pattern (or Stitching → Fill pattern):
    • automatic — looks at the shape's compactness (4πA/P²: 1.0 for a circle, 0.22 for a long sliver) and gives round or blobby areas a contour fill, everything elongated straight rows along its long axis;
    • straight rows — classic tatami, the old behaviour;
    • contour — concentric rows following the outline, generated by repeatedly buffering the shape inwards. Each next ring is re-opened at the point nearest the previous row's end, so the hop is one row spacing, not a jump. On the sample donut this drops jumps from 51 to 5;
    • spiral — the same rings blended into one unbroken path: consecutive rings are resampled to a matching point count and interpolated along the way round, so the row drifts inwards exactly one spacing per turn. Falls back to contour where the shape splits in two;
    • radial — rows from the middle outwards. A constant angular step would crowd the centre and gap at the rim, so the radius is split into bands that grow 35% at a time with the spoke count set by each band's outer edge, alternate bands offset by half a spoke so the seams don't line up, and the middle disc filled concentrically. Press A and click to put the centre where you want it.
  • Fill — straight rows are generated in a frame rotated by the fill angle, cut against the polygon, staggered per row so the needle penetrations don't line up into visible channels, then ordered greedily. If the hop to the next row stays inside the shape it becomes travel stitches; if it crosses a hole, the engine walks the inner boundary instead of trimming. Underlay is a running stitch on an inset copy.
  • Satin — rails are built by offsetting each sampled point along its own local normal, so left[i] and right[i] are always exactly opposite and every stitch is one column wide. (offset_curve was the obvious route and is wrong for closed rings: the inner and outer curves come back with different lengths, different start points and sometimes opposite winding, so pairing them by normalised distance made the zig-zag wander across the whole shape instead of following the edge.) Satin on a filled area follows its outermost contour only by default. An area has three kinds of contour and the Which edges setting picks how far to go: outer edge only (the silhouette), outer edge + parts inside holes (islands — their exterior is technically an outer ring but reads as an internal edge), and every edge, holes included. Holes in a traced design are usually where the next colour sits, so stitching them draws a column straight down the colour border. Projects saved with the older include_holes flag migrate on load.
  • Clean-up — stitches shorter than min_stitch_length are dropped (needle breakers), stitches longer than 12 mm are split (format limit), lock stitches are added at both ends of every block.
  • Optimisation — same-colour layers merged into one machine job, objects ordered nearest-neighbour, colour blocks kept contiguous so the operator rethreads once per colour.

Outlining a design

Shape → Add outline around design (silhouette) unions the selected areas first, so an edge shared by two colour regions ends up inside the union and is never stitched — only the circumference comes back, as one satin object. With nothing selected it outlines the whole design.

Traced regions rarely share exact coordinates (a fraction of a millimetre of background pixels sits between them), which is enough to stop a plain union from merging them and would put the outline back on the colour border. silhouette() therefore dilates by 0.3 mm, unions, and erodes back — anything closer than 0.6 mm merges, the real outer edge doesn't move. On the two-rectangle test design: 470 mm of region boundary in, 312 mm of actual outline out, one closed path.

Panels

Both side panels rebuild only when their structure changes — the layer tree on a change of layers/objects/names/colours/visibility, the properties panel on a change of selection. Rebuilding on every event cleared the widget on the way back from your own click, which left the layer tree with no current item and made reorder, delete and colour do nothing. The active layer is now tracked by uid independently of what the tree widget thinks is current, and is shown under the tree.

Stitch preview is on by default; Tab toggles back to the flat shape view for editing.

Editing an existing design

The properties panel edits whatever is selected, at any time — one object or fifty. Editors are only rebuilt when the selection changes, so a value never gets ripped away mid-keystroke, and one turn of a knob is one undo step rather than one per click.

Switching stitch type (1 fill, 2 satin, 3 run, or the combo box) carries over the settings the types share and archives the ones they don't, so switching back restores exactly what you had tuned. The archive is saved in the project file.

Regeneration runs in a worker thread on a snapshot of the document: a progress bar and a Stop button appear in the status bar, editing stays responsive, and a new edit cancels the running calculation and restarts it.

Why the file "has more stitches" than the preview

Needle penetrations always match exactly. A stitch file additionally stores each jump, trim and colour change as its own record, and many stitch viewers report that command total as the stitch count. For the sample design:

design DST PES JEF
stitches (needle down) 4481 4481 4481 4481
+ jumps / trims / colour changes 69 / 49 / 2 52 / 51 / 2 66 / 2 / 2
file command total 4602 4587 4552

Size is identical in all of them. The export dialog now reports both numbers and the breakdown. Thread length in the design report is what the needle actually sews — travel inside a block counts, jumps between blocks don't — so a viewer that adds jump distance will quote a slightly larger figure.

Escaping an operation

Drawing tools are one-shot: after you finish a rectangle, a freehand path or a direction drag, the mouse goes back to the selection pointer, so a stray click can never redraw or re-angle anything. Turn that off with Tools → Keep drawing tool active after use.

Esc (also right-click → Cancel current operation) works in stages: abandon the drag in progress and roll back whatever it already changed → return to the selection tool → clear the selection. A press that ends without a drag leaves no undo step and triggers no recalculation.

Save / export / print

  • .stitchproj — zip with document.json (WKT geometry + parameters). Editable, diffable, forward-compatible.
  • Stitch file — any format pystitch can write (the dialog is built from supported_formats(), so a pystitch update adds formats with no code change), centred on origin by default. Colour-only sidecars (COL/EDR/INF) are filtered out.
  • PDF sheet — File → Save design sheet as PDF writes the identical page without involving a print system, which is the reliable route on a machine with no printer configured.
  • Print — design drawn stitch by stitch at true size when it fits, plus a thread colour index in sewing order (job number, swatch, matched thread name, stitch count, thread length).

Shortcuts

V/R/E/P/F/C/A tools (A = fill direction) · 1/2/3 fill / satin / run · Tab stitch preview · Ctrl+Z/Y undo/redo · Ctrl+X/C/V/D cut/copy/paste/duplicate · Ctrl+G grid copy · H mirror · J flip · Ctrl+←/→ rotate 90° · arrows nudge (Shift = ×10) · Ctrl+' grid · Ctrl+; snap · Ctrl+0 fit hoop · F5 regenerate · F1 all shortcuts. Middle-drag or Alt-drag pans, wheel zooms, right-click is the full context menu.

Known gaps / next steps

  1. Regeneration recomputes the whole plan; per-shape caching (hash of geometry + params) would make single-parameter tweaks instant on big designs.
  2. Satin rails are derived from a centre line; hand-drawn two-rail satin columns and node editing are the next real feature.
  3. No node/bezier editing yet — shapes are polygons after import.
  4. Contour, spiral and radial cover the "follow the shape" cases. Still missing is a guided fill — rows that follow a user-drawn guide line through the shape, which is what you want for a curved leaf or a ribbon; the plumbing (per-shape parameters, the A tool, per-polygon dispatch in tatami_fill) is already in place for it.
  5. Thread chart is a small generic one; swap core/threads.py for a real manufacturer chart (Madeira/Isacord) for accurate spool numbers.
  6. Packaging: PyInstaller --onefile per OS gives non-technical users a double clickable app; the code has no data files, which keeps that build trivial.