Files
Graphite/plotter-upload-spec.md
Keavon Chambers 9537a4ae03 Plotter
2026-07-18 00:15:36 -07:00

140 lines
8.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Spec: "Send to Plotter" — upload the current document as SVG to a LAN print server
## What this is
We are running a conference booth where visitors draw vector artwork in Graphite at demo
stations, and their drawing gets physically plotted with a pen on letter paper by a Cricut
machine as a takeaway. A small HTTP print server (a separate, already-finished project) drives
the Cricut; it accepts SVG jobs over the LAN and manages a queue with a live dashboard.
The missing piece, and the task for you, is on the Graphite side: an in-app action that exports
the current document as SVG and POSTs it to that print server. Today we have to save an SVG file
and manually upload it via the dashboard; visitors should instead be able to click one thing
inside Graphite.
## Booth context (why the design looks like this)
- Four Windows 11 demo stations run Graphite. The booth LAN is an isolated switch with **no
internet**: static IPs on `192.168.77.0/24` (stations are `.10`, `.20`, `.30`, `.40`), blank
gateway. Because there is no internet, each station runs Graphite from a **local dev server**
(`npm start`, i.e. an `http://localhost` origin), not from the production website.
- The print server runs on the station connected to the Cricut, listening on port `4747`. At
the booth it is expected at `http://192.168.77.10:4747`, but the host may change, so the
endpoint URL must be user-configurable and persisted. For development, the natural default is
`http://localhost:4747`.
- Sending a job is safe and non-destructive: the queue is human-gated twice (a booth attendant
resumes the paused queue from the dashboard, and each plot additionally waits for a physical
Go button press on the machine). Graphite only needs to fire the upload and report success or
failure; queue management, previews, and status all already exist on the server's dashboard.
## The endpoint
`POST http://<host>:4747/api/jobs`
Two accepted body forms; **use the raw form** (simplest):
1. **Raw SVG body** — any `content-type` that is not `application/json` (use
`image/svg+xml`). The job name is taken from the `?name=` query parameter.
2. JSON body (`content-type: application/json`): `{ "svg": "<svg …>", "name": "…", "options": { … } }`.
Example of the raw form:
```
POST http://192.168.77.10:4747/api/jobs?name=Alices%20drawing
content-type: image/svg+xml
<svg xmlns="http://www.w3.org/2000/svg" …>…</svg>
```
Server behavior:
- **Success**: `201` with JSON `{ "id": "<job id>", "name": "<name>", "status": "queued" }`.
- **Validation failure**: `400` with JSON `{ "error": "request body must contain an <svg>" }`
(the body must match `/<svg[\s>]/i`). Other errors return `500` with `{ "error": "…" }`.
- **Body limit**: 25 MB.
- **CORS**: fully open. Every response carries `access-control-allow-origin: *`, and `OPTIONS`
preflights are answered with `access-control-allow-methods: GET,POST,DELETE,OPTIONS` and
`access-control-allow-headers: content-type` (plus `access-control-allow-private-network:
true` for Chrome's private-network-access preflight). A plain browser `fetch` works.
- If a `name` is omitted the server invents one; still, always send the document name so the
attendant can tell jobs apart on the dashboard.
- Do **not** send job options from Graphite (paper size, rotation, etc. exist as query
parameters, but the booth-wide defaults are configured on the server; the client staying
dumb is a feature).
## What the plotter does with the SVG (sets expectations for the export)
- The artwork is auto-scaled to fit 7.5×10" (letter paper minus margin) preserving aspect
ratio, and auto-rotated to portrait when clearly landscape. Absolute units and document size
in the SVG are irrelevant; only the aspect ratio and the shapes matter.
- Everything is drawn with a **pen**: every path renders as its outline. Fills are not filled
in; a filled shape plots as its contour.
- The server already strips Graphite's artboard background: an exported artboard produces a
background `<rect>` immediately before a `<g clip-path="url(#artboard-…)">` group, and the
server removes exactly that rect. So exporting a document with an artboard is fine as-is.
Known limitation: a solid background drawn as anything else (a giant `<polyline>`, a path)
is NOT stripped and would be plotted, but that is a server concern, not yours.
- Use the same SVG serialization as the existing file export (File > Export); the server is
known to handle that output. Do not invent a new export path.
## What to build in Graphite
1. A menu action (e.g. **File > Send to Plotter…**, near Export) that opens a small dialog:
- **Server address** text field, persisted across sessions (default `http://localhost:4747`).
Accept a bare `host:port` or full origin; normalize to `<origin>/api/jobs` internally.
- **Job name** text field, defaulting to the document name.
- A **Send** button.
2. On send: export the current document to an SVG string exactly as File > Export SVG would
(whole document / all artboards, default settings), then
`fetch(endpoint, { method: 'POST', headers: { 'content-type': 'image/svg+xml' }, body: svg })`
with the name in the query string.
3. Feedback:
- Success (`201`): a brief confirmation (e.g. a toast/dialog: "Sent to plotter queue as
'<name>'").
- Failure: show the reason. Distinguish "could not reach the server" (network error —
wrong address, server not running) from a server-reported error (`400`/`500` JSON
`error` field). Booth attendants are non-experts under time pressure; the message should
say what to check ("Is the print server running at <address>?").
- A pending state on the button while in flight; sends should not be double-fireable.
4. Non-goals: no queue status display, no job management, no auth, no retry logic, no
settings beyond the address field. The server dashboard covers all of that.
## Testing without the plotter
The print server is a zero-dependency Node ≥ 20 project; run `node bin/cricut-print-server.mjs
serve` from its repo and it listens on `:4747` even with no Cricut hardware or Design Space
running — submitted jobs simply sit in the paused queue, visible with previews at
`http://127.0.0.1:4747/`. That is the ideal end-to-end check: send from Graphite, see the job
card appear with the right name and a correct preview.
If you don't have that repo, a sufficient mock is:
```js
require('node:http').createServer((req, res) => {
let b = '';
req.on('data', (c) => (b += c));
req.on('end', () => {
const cors = { 'access-control-allow-origin': '*', 'access-control-allow-headers': 'content-type', 'access-control-allow-methods': 'GET,POST,DELETE,OPTIONS', 'access-control-allow-private-network': 'true' };
if (req.method === 'OPTIONS') { res.writeHead(204, cors); return res.end(); }
const ok = /<svg[\s>]/i.test(b);
res.writeHead(ok ? 201 : 400, { ...cors, 'content-type': 'application/json' });
res.end(JSON.stringify(ok ? { id: 'test1', name: new URL(req.url, 'http://x').searchParams.get('name'), status: 'queued' } : { error: 'request body must contain an <svg>' }));
console.log(req.method, req.url, b.length, 'bytes');
});
}).listen(4747);
```
## Gotchas
- **Mixed content**: a Graphite instance served over HTTPS (the production site) cannot
`fetch` an `http://` LAN address — the browser blocks it silently-ish. This is fine for the
booth (stations run `http://localhost` dev builds, and localhost is a secure context allowed
to reach private hosts), but don't be confused if a test from the production site fails;
consider mentioning it in the failure message if `location.protocol === 'https:'` and the
target is `http:`.
- **Chrome private network access**: Chrome sends a special preflight when a page reaches into
a private network. The server answers it (`access-control-allow-private-network: true`), so
this should just work; noted here in case a future Chrome version tightens behavior.
- The SVG can be large (procedural documents); it is sent as one POST body, well under the
25 MB limit in practice. No chunking or compression needed.