Metadata-Version: 2.5
Name: pac-vector
Version: 0.3.0
Summary: Local stdio MCP server for SVG editing via in-memory virtual documents, with a live browser preview
Requires-Python: >=3.11
Requires-Dist: fonttools>=4.40
Requires-Dist: lxml>=5.0
Requires-Dist: mcp[cli]<2.0.0,>=1.10.0
Requires-Dist: pillow>=10.0
Requires-Dist: resvg-py>=0.5.0
Requires-Dist: skia-pathops>=0.8
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == 'dev'
Description-Content-Type: text/markdown

# pac-vector

Local stdio MCP server: an SVG editor and **Lottie animator** over in-memory **virtual
documents** (one per "window", like Inkscape), with a **live browser preview** of every edit.

- Content goes in as SVG markup; structure (layers, groups, masks, z-order) has typed tools.
- Animation documents turn finished SVG artwork into Lottie: keyframes or presets per
  layer, frame renders for the agent, `.json` export, and a player with a timeline.
- Layers are real Inkscape layers, so exported files open in Inkscape with layers intact.
- Path booleans (union, difference, intersection, exclusion, division) via Skia PathOps.
- Every edit is atomic (a failed call changes nothing), one undo/redo step, and autosaved:
  windows of a crashed session reopen in the next one.
- `render` returns a PNG the agent can look at; rendering is resvg (no browser, no Cairo).

## Install

Same shape as pac-pincers: a pip package, run as a module by whatever Python it
was installed into.

```powershell
python -m pip install --upgrade pac-vector
claude mcp add pac-vector --scope user -- python -m pac_vector
```

As a plugin `.mcp.json` entry (mirrors the datatopia plugin's pac-pincers entry):

```json
"pac-vector": {
  "type": "stdio",
  "command": "${PAC_VECTOR_PYTHON:-python}",
  "args": ["-m", "pac_vector"],
  "env": {}
}
```

Development:

```powershell
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install -e ".[dev]"
.\.venv\Scripts\python.exe run_tests.py -q
.\.venv\Scripts\python.exe -m build          # dist/pac_vector-<ver>.tar.gz + .whl
```

## Tools (22)

| Group | Tools |
|---|---|
| Documents | `doc_create` (svg: blank, file, markup, duplicate · anim: from SVG artwork), `doc_list`, `doc_set` (size, background; svg: units, viewBox, fit; anim: fps, duration), `doc_export` (svg/png; anim: Lottie `.json` or one frame), `doc_close` |
| Reading | `inspect` (svg outline or xml; anim layers + keyframes), `render` (svg PNG, crop to ids + bbox; anim: a frame or a strip of frames) |
| Editing | `add` (markup → layer/group, defs auto-routed; SMIL animations into any element), `update` (svg: attrs/style/text/transform/label/hidden/locked/text-on-path · anim: layer in/out, static transform, name, hidden), `replace`, `remove`, `arrange` (z-order, move, duplicate, offset), `import_svg` (file / other window / markup → editable group or image; anim: new layers) |
| Structure | `group` (also new layers), `ungroup`, `mask` (mask or clip from id or markup), `unmask` |
| Geometry | `boolean` (union, difference, intersection, exclusion, division), `align` (align edges/centers, distribute evenly or by gap; relative to selection, page, first, last or an element) |
| Animation | `animate` (keyframes for position, anchor, scale, rotation, opacity, trim start/end/offset, fill/stroke colour, stroke width — or presets: fade, pop, slide, draw on/off, spin, pulse, shake — with per-layer stagger) |
| History | `undo`, `redo` |

## Animation documents (Lottie)

```
doc_create(kind="anim", source=<svg doc id | .svg path | markup>, duration="3s", fps=30, split="elements")
animate(doc, layers=["disc"], preset={"name": "pop_in", "start": 0, "duration": "0.8s"})
animate(doc, layers=["ring", "check"], preset={"name": "draw_on", "start": "0.3s"}, stagger="0.4s")
animate(doc, layers=["disc"], property="rotation", keyframes=[{"t": 30, "value": 0}, {"t": "2s", "value": 90, "ease": "back"}])
render(doc, frames=[0, 15, 45, 89])          # look at frames
doc_export(doc, path="intro.json")            # Lottie
```

- Each element of the artwork becomes a layer (`split`: elements / layers / single). Shapes
  are baked to paths in composition pixels with their paint resolved; anchor and position
  start at the artwork's centre, so scale and rotation pivot there.
- Times are frames (`12`, `"12f"`) or seconds (`"0.4s"`). A keyframe's `ease` shapes the
  segment that starts at it: `linear`, `ease*`, `smooth`, `snap`, `back`, `back-in`,
  `anticipate`, `hold` or a `[x1, y1, x2, y2]` cubic-bezier.
- Not carried over (reported on import): text (convert to paths first), images, `<use>`,
  clip paths and masks. Gradients become their average colour.
- `render` draws frames with resvg from the same model, an approximation of lottie-web
  (trim paths trim strokes; Lottie also trims fills). The browser preview plays the real
  exported JSON with lottie-web.

## Preview

`doc_create` / `doc_list` / `inspect` print the preview URL (`/#<doc>` for SVG,
`/anim#<doc>` for animations). The SVG page has a tab per document, outlines the
elements each edit touched, and shows (and copies) the id of anything you click. The
animation page plays the Lottie with lottie-web (loaded from cdnjs): play/pause, frame
step, loop, speed, a range bar to loop a section (handles, drag, I/O keys), and one track
per layer with its animated properties and clickable keyframes. The server lives inside
the MCP process: it starts on the first `doc_create` and dies with the session.

## Environment

| Variable | Default | Meaning |
|---|---|---|
| `PAC_VECTOR_PORT` | `0` (random) | Pin the preview port (falls back to random if busy) |
| `PAC_VECTOR_OPEN_BROWSER` | off | `1` opens the preview in the default browser on first start |
| `PAC_VECTOR_ROOT` | server cwd | Base directory for relative export/import paths |
| `PAC_VECTOR_LOG` | `WARNING` | Log level (stderr only) |
| `PAC_VECTOR_AUTOSAVE` | on | `0` disables the crash-recovery mirror |
| `PAC_VECTOR_AUTOSAVE_DIR` | `<tmp>/pac-vector-autosave` | Where the mirror lives (`<pid>/<doc>.svg` + `.json`) |

## Notes

- stdout is the JSON-RPC pipe: nothing may print to it. `tests/test_stdout_safety.py`
  scans the package for `print()` / `sys.stdout`, and `tests/test_stdio.py` runs a real session.
- Native modules are loaded before the stdio loop starts (`render.warm`): on Windows a
  first native load from inside the FastMCP loop can hang (see pac-pincers' `_excel.py`).
- Bounding boxes are measured from pixels (two-pass render), so they include strokes and
  filters and work for text and curves; content further than two page-widths off-canvas isn't found.
  `align` uses them, so it aligns what you see.
- Booleans use fill geometry only (strokes, text and images don't take part); the result
  is a `<path>` in the bottom operand's place, styled like it, keeping its id.
- Animations are SMIL: the browser preview plays them; `render` / PNG export show the
  static first frame.
- Version lives only in `pyproject.toml`; a release is a commit named after the version.
