Turn a photo (or a procedural drawing) into layered, pen-plotter-ready SVG, then preview it and plot it — all in one app.
PlotterForge is a browser-based studio for pen plotters, in the spirit of DrawingBotV3. You import images as movable layers, pick a path-finding style (stippling, hatching, TSP art, flow fields, tessellations, dithers, and more), arrange everything on a real page with real pens, and either export an SVG or drive a Grbl plotter directly.
This project was vibe-coded — most of the code was written with an AI assistant rather than typed by hand. That's the honest origin story, and you should factor it into your expectations: the architecture is pragmatic, not academic, and there are corners that reflect "what worked" over "what's textbook."
What it isn't, though, is untested. I use PlotterForge as my actual plotting tool. Every style, the layer system, the pen sets, and the live Grbl plotting have been run against real hardware and real drawings, repeatedly, because I needed them to work for my own art. So: AI-written, human-hammered.
- 50 path-finding styles. Samplers (Voronoi, LBG, adaptive, Poisson-disk) combined with looks like stippling, dashes, shapes, triangulation, trees, diagrams, and TSP, plus grid halftone, spiral, hatch, sketch, streamlines, dither halftone, circle packing, differential growth, quadtree mosaic, and raster-driven tessellations. Each style's controls are generated from a typed schema, so nothing is hidden.
- Layer-based composition. Photos come in as non-destructive raster layers (EXIF-correct, never auto-cropped). Move, scale, rotate, fit, or fill them; the path-finding follows whatever transform you set.
- Click-to-segment (SAM2). Click a region of your source image and run a different style on it than on the rest.
- Real page, real pens. Set the drawing area in physical units, define a multi-pen set with per-pen width and colour, preview flat-nib calligraphy, and plot each pen as its own pass with guided pen changes.
- Plot or export. Live plot preview with a time estimate, direct Grbl serial plotting with a safe Stop/Resume, or a multi-layer millimetre SVG for Inkscape, vpype, or your plotter's own software.
- Versioned projects. Immutable snapshots with thumbnails, kept under
~/.plotterforge/, and a project browser that shows the whole workspace as a grid of rendered pages rather than a list of names. - GPU when you have one. PyTorch acceleration (CUDA on Windows, MPS on Apple Silicon) for the sampling-heavy styles, with a CPU fallback that just works.
- Cavalry bridges. Stream SVG frames live from Cavalry, or bake a Cavalry composition into a reusable tone-responsive tessellation style.
Full feature list: FEATURES.md.
Every style ships with a preview so you can eyeball the look before committing. A few, to give you the range:
![]() |
![]() |
![]() |
| Voronoi stippling | Hatch | Streamlines |
![]() |
![]() |
![]() |
| TSP art | Circle packing | Tessellation |
The full picker, with live previews, is built into the app:
You don't need to touch a terminal after the one-time setup, and you never need Node or a build step — the app's interface is pre-built and committed.
- Install the two prerequisites below (uv and Node.js — both have normal installers).
- Run the setup script for your OS once.
- Run the start script and open http://localhost:7438 in your browser.
That's it. Everything after that happens in the browser: import a photo, choose a style, arrange it on the page, hit plot or export. A full illustrated manual is built into the app and opens from inside it.
engine/— the conversion engine (styles, samplers, pens, drawing area, version control, GPU/CPU backends, SVG output). Pure Python, runs headless, fully testable.frontend/— a Svelte 5 single-page app.npm run buildwrites toweb/static/app, which is committed so end users skip the JS toolchain.plotter/— the plotter side: SVG flattening, path ordering, travel estimates and the parsed-path cache. Pure geometry, no Flask, so profiling and scripts can import it.web/— the Flask application.routes/holds one blueprint per area (projects, composition, machine, plotting);services/holds the operations that own real orchestration and import no Flask;session.pyis the mutable application state;server.pyis the remaining assembly plus the Grbl driver.
Two things about web/ worth knowing before you change it. It is deliberately a
single-user, single-machine server — one open project, one plotter, process-wide
state — so per-request sessions and multi-lock schemes are the wrong shape here.
And path finding runs synchronously in the request thread: a dense render can
hold one HTTP request open for minutes. It reports progress over the event
stream and blocks project switches while it runs, but it cannot be cancelled,
and a proxy with a short idle timeout will give up on it. Moving it to a
background job is a known, deliberate not-yet.
Viewport crop, scale, rotation and occlusion mirror the export engine through Python-generated conformance fixtures; the intentional flat-nib preview difference is documented in the viewport geometry contract.
Style controls are declared as typed schemas in engine/params.py, and the
manual's parameter reference is generated from them. See
Development.
- Windows 10/11 with an NVIDIA GPU and a current driver, or macOS on Apple Silicon
- uv — the Python project manager (handles Python 3.13 for you; no Conda)
- Node.js with npm
| Platform | Run |
|---|---|
| Windows | setup-windows.bat |
| macOS | ./setup-macos.command (or double-click in Finder) |
Setup installs the right PyTorch build for your machine, a pinned SAM2 and its default checkpoint, and the frontend dependencies. It then runs a real segmentation inference to confirm things actually work before saying "done". Rerun it after pulling dependency or frontend changes.
| Platform | Run |
|---|---|
| Windows | start-windows.bat |
| macOS | ./start-macos.command (or double-click in Finder) |
Open http://localhost:7438. The launchers are offline and side-effect-free: they never install, sync, build, download, or kill processes.
When a UI problem is difficult to reproduce, start the same launcher with its optional debug flag:
| Platform | Run |
|---|---|
| Windows | start-windows.bat --debug |
| macOS | ./start-macos.command --debug |
Debug mode writes two files under ~/.plotterforge/logs/ (on Windows,
%USERPROFILE%\.plotterforge\logs\):
debug-actions-*.jsonlrecords meaningful clicks, control changes, canvas gestures, shortcuts, API outcomes/request IDs, browser errors, and compact layer-state transitions in chronological JSON Lines.plotter.logrecords the correlated server and worker operations.
Give both files to the LLM investigating the problem. Action recording is off for a normal launch. It never stores request/response bodies, image or SVG contents, imported filenames, or text-field values; file inputs retain only media type, byte size, and extension. Debug traces can still reveal workflow structure and project/layer IDs, so review them before sharing.
The default server is local-only. It binds to 127.0.0.1, accepts only
localhost and loopback Host headers, and rejects state-changing browser
requests whose Origin does not match the server.
When intentionally exposing PlotterForge through a LAN address or reverse proxy, configure both the listening interface and the public names:
PLOTTER_HOST=0.0.0.0 \
PLOTTER_TRUSTED_HOSTS=plotter.example,192.168.1.25 \
PLOTTER_ALLOWED_ORIGINS=https://plotter.example \
uv run --locked --no-sync python -m web.serverPLOTTER_TRUSTED_HOSTS is a comma-separated hostname/IP allowlist; do not add
untrusted wildcard names. PLOTTER_ALLOWED_ORIGINS is a comma-separated list
of exact http:// or https:// origins and is only needed when the browser's
public origin differs from Flask's request origin, as it can behind a TLS
proxy. Originless requests remain accepted for non-browser integrations such
as Cavalry and the CLI, while requests explicitly marked by a browser as
cross-site are rejected.
uvor Node.js not found — install it, then rerun the setup script.- "Run setup first" — the
.venvis missing or stale; rerun setup. Port 7438 is already in use by PID …— another instance is running. Stop that PID and relaunch (the launchers won't kill it for you).- CUDA/MPS unavailable — check your GPU driver (Windows) or that you're on Apple Silicon (macOS), then rerun setup.
setup is incomplete: missing …— SAM2, Torch, or the checkpoint is absent. Rerun setup; the server never installs anything at runtime.
The in-app Troubleshooting chapter covers first-plot calibration and the safe Stop/Resume procedure.
Preview the toolpath, check the time estimate, and either plot over serial or export SVG.
The full manual ships with the app and is served locally while it runs.
| Guide | For |
|---|---|
| Manual | Start here — a tour of the whole workflow |
| Tutorials | Three reproducible start-to-finish artworks |
| Choose a style | Decision guide across all 50 styles |
| Parameter reference | Every control, default, range, and description |
| Pens, paper & plotting | Operators: calibration, pen changes, safety |
| Cavalry tessellations | Bake Cavalry comps into custom styles |
image / generator ──▶ engine (style) ──▶ Drawing ──▶ multi-layer mm SVG ──▶ plot / export
Projects, versions, models, and installed tessellations live under
~/.plotterforge/ (migrated automatically from the older ~/.plotter_studio/
if it's there).
# Backend tests
uv run --no-sync pytest
# Frontend dev server with hot reload (proxies /api to Flask on :7438)
cd frontend && npm run dev
# Frontend build (regenerates web/static/app)
cd frontend && npm run build
# End-to-end tests (Playwright, isolated HOME + locked env)
cd frontend && npm run e2e
# Regenerate the manual's parameter reference after editing engine/params.py
uv run --no-sync python tools/build_docs_reference.py
# Check the manual for broken links and stale screenshots
uv run --no-sync python tools/check_docs.py
# Retake every manual screenshot from the live app (opt-in)
cd frontend && DOCS_CAPTURE=1 \
E2E_BACKEND_CMD="uv run --no-sync python -m web.server" \
SAM2_CHECKPOINT="$HOME/.plotterforge/models/sam2.1_hiera_tiny.pt" \
npx playwright test docs-capturedocs-capture.spec.ts rebuilds each documented app state and rewrites
web/static/docs/img/*.png, so the manual's screenshots always show the
current UI. Rerun it after a change that alters what those states look like.
Keep the two env vars: the e2e suite's default backend has no Torch, SAM2, or
checkpoint, so capturing against it bakes a CPU · numpy badge and a red
"setup is incomplete" error into the manual.
The deterministic CPU/MPS/CUDA/browser profiling suite is documented in docs/profiling.md; CI publishes a performance profile on every pull request. Product notes live in docs/product-roadmap.md.
Prefer to plot from Inkscape? The bridge/ setup exposes a plotter connected to a Raspberry Pi as a local virtual serial port over Tailscale, so the UUNA TEK / iDraw extension works wirelessly. The bridge holds the serial port while running, so stop it before plotting from PlotterForge.
engine/ Conversion engine: styles, samplers, pens, drawing area,
version control, GPU/CPU backend, SVG output
plotter/ Plotter side: SVG → polylines, path ordering, travel estimates,
parsed-path cache
integrations/ Third-party adapters (SAM2 segmentation)
frontend/ Svelte 5 SPA; `npm run build` → web/static/app
web/ Flask app: routes/ blueprints, services/ operations, Grbl driver
docs/ Developer docs: Cavalry guide, profiling, roadmap, design notes
tests/ Backend test suite (pytest)
tools/ Repo-local entry points (docs generator/checker)
profiling/ Deterministic performance profiling suite
bridge/ Wireless serial bridge: plot from Mac Inkscape via a Pi
cavalry/ Cavalry UI script for live capture + tessellation baking
pyproject.toml uv manifest (engine + web deps; cuda/mps + sam2 extras)
uv.lock locked dependency tree









