# drawing-machine

A drawing engine built in layers, after the the paint simulator runs showed that a
language model writing stroke coordinates draws mechanically (the SketchAgent
paper reports the same). Local only.

| layer | file | does |
|---|---|---|
| material | `material/surface.py` | dry media in PyTorch: paper tooth height field, graphite 4H to 8B, vine and compressed charcoal, stump, kneaded eraser (lifts, leaves a ghost in the valleys), fixative. Pressure decides how deep into the tooth a tool reaches; pigment saturates; Beer-Lambert optics. Deterministic per seed. |
| motor | `motor/hand.py` | a gesture (control points, pressure profile, character, seed) becomes a trajectory: smooth path, minimum-jerk speed, pressure coupled to speed, tremor that grows with slowness, attack and release, breaks with re-entry, overshoot, hooks, tilt onto the side. Characters: neutral, decisive, searching, hesitant, side, hatch. |
| planner | `planner/plan.py` | FRIDA-style: propose strokes where the residual is (hatching across the form, contours along it), optimise geometry and pressure through a soft differentiable stand-in, calibrate the stand-in per pass against strokes drawn by the real motor and material. |
| agent | `agent/draw_target.py` | a value target in a box, passes chosen as an agent would (tool, character, count, length), rounds of plan, draw, look, re-plan. Writes the sheet, per-pass frames, and a record of every stroke as a gesture. |
| tests | `tests/specimen.py` | grades, charcoal, characters, hatching ladder, stump, eraser, darkness ceiling. |

## Measured (specimen, 2026-10-07)

- paper L* 93.5; compressed charcoal block 16.8 (1 layer) to 9.3 (4 layers): a real black (the the paint simulator easel's chalk floored near 37)
- graphite 8B, three layers of hatching: 35.5; vine charcoal block 48.9, stumped 36.2
- render speed: the specimen (about 260 gestures) in 5 s on MPS at 1200 x 900

## State of the planner (hands_v2: 1380 strokes, 9 rounds, 7 min)

- the remaining error halves across the rounds (0.110 to 0.051 mean residual) and light shapes are reserved as paper
- it does not yet read as hands. Open problems, in order:
  1. the planning grid (0.25 px per unit) is too coarse for fingers; needs local, sparse rendering so it can run finer
  2. the side-of-charcoal lay-in renders as dark short blobs; its stroke shape and calibration need work
  3. hatch direction comes from a noisy target; the reference is a stipple drawing, a poor value plan
  4. no motor prior on the planner: strokes are optimised as curves, then the hand is added after

## hand/: Hands as the motor (from 2026-10-07)

The PyTorch planner above is parked. The motor is now the Hands engine, vendored read-only in
`vendor/hands/` (snapshot of parallaxmetrology/hands; see SOURCE.txt). Generated images are targets
only; every mark on a canvas is a recorded native stroke or eraser pass.

| tool | does |
|---|---|
| `prepare.py` | image -> target (`.rgba` + `.size.json`) at a width (900-1100 px: Hands' mark scale is fixed) |
| `compose.py` | value plan -> target: `place` (oval, `blur`, `values`), `mass` / `blob` (authored shapes), `lighten`, `from_canvas`; base may be `{"paper": [W, H]}` (no image) |
| `run.mjs` | draw a target with a hand (`--init` continues on an existing canvas) |
| `lift.py` | kneaded-eraser take-back in a box (leaves a ghost in the tooth) |
| `intend.mjs` | intention kernels on a canvas: reserve-paper, connect-mass, soften-edge, break-edge; guards; sequential commitments; exact replay |
| `propose.py` | critic read.json + canvas -> intention job (+ overlay png to read before running) |
| `render.py` | `.f32` canvas -> png |
| `lineage_video.py` | lineage.json -> video of every full-image state in order: kept spine vs branches (with reasons), generator nodes, Hold, detail crops; end card per part saved as png |
| `step.py` | the ledger: one line per pull between platforms (tool, platform, hashed in/out, prompt verbatim, who); `--show` writes a local ledger.html |
| `museum.py` | search open-access collections (AIC; Met answered 410) for public-domain references; `get` downloads one on request |

Tests: `node tests/test_intend.mjs`. Worked sheets: `out/sheet4/NOTES.md`, `out/sheet5/NOTES.md`.

## The microsite
`python3 site_build.py` builds `site/` (open `site/index.html`): overview, how it works, the method, the three
case studies (sheet 4, sheet 5, still life), tests and findings, and the the paint simulator background. It copies and
resizes every image and video it uses into `site/assets/` and renders the project's own notes, critiques and Hold
records. `sh publish_to_site.sh` rebuilds it and copies `site/` into `~/Documents/parallaxmetrology/drawing-machine/`
(the site folder's microsite pattern); it copies and does not deploy. `--noindex` adds a robots noindex to every page.
Home-folder paths are replaced with `~` in copied records; candidate logs, stroke records, canvases and code are not copied.
