- Python 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| stitchstudio | ||
| tests | ||
| .gitignore | ||
| .pre-commit-config.yaml | ||
| .python-version | ||
| LICENSE | ||
| pyproject.toml | ||
| README.md | ||
| run.py | ||
| screenshot-contour-fill.png | ||
| screenshot-fill-patterns.png | ||
| screenshot-icons.png | ||
| screenshot-layers-preview.png | ||
| uv.lock | ||
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.
- automatic — looks at the shape's compactness (
- 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]andright[i]are always exactly opposite and every stitch is one column wide. (offset_curvewas 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 theWhich edgessetting 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 olderinclude_holesflag migrate on load. - Clean-up — stitches shorter than
min_stitch_lengthare 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 withdocument.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
- Regeneration recomputes the whole plan; per-shape caching (hash of geometry + params) would make single-parameter tweaks instant on big designs.
- Satin rails are derived from a centre line; hand-drawn two-rail satin columns and node editing are the next real feature.
- No node/bezier editing yet — shapes are polygons after import.
- 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. - Thread chart is a small generic one; swap
core/threads.pyfor a real manufacturer chart (Madeira/Isacord) for accurate spool numbers. - Packaging: PyInstaller
--onefileper OS gives non-technical users a double clickable app; the code has no data files, which keeps that build trivial.