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

8.0 KiB
Raw Permalink Blame History

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 ''").
    • 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 ?").
    • 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:

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.