461 lines
33 KiB
Markdown
461 lines
33 KiB
Markdown
# VectorCraft MCP server
|
||
|
||
`vectorcraft-cli mcp` runs a [Model Context Protocol](https://modelcontextprotocol.io) server on stdio
|
||
(newline-delimited JSON-RPC 2.0, protocol `2025-06-18`; `2025-03-26` and `2024-11-05` also accepted). Agents use it to
|
||
draw, inspect and look at VectorCraft documents.
|
||
|
||
It has two backends:
|
||
|
||
| Mode | How | What works |
|
||
|---|---|---|
|
||
| **Remote** | `vectorcraft-cli mcp --connect 127.0.0.1:7979` (the app must run with `vectorcraft --control 7979`) | Everything. Tool calls are forwarded over the [control protocol](control-protocol.md), so you watch the app change live. |
|
||
| **Headless** | `vectorcraft-cli mcp --headless` | An in-process engine session with a CPU renderer. Everything except the UI-only tools (`inspect_ui`, `type_text`, `open_panel`, `screenshot {window:true}`). |
|
||
|
||
With no flag, the server tries `127.0.0.1:7979` and falls back to headless. Logs go to stderr; stdout carries
|
||
only protocol messages.
|
||
|
||
## Build and register
|
||
|
||
```sh
|
||
cargo build --release -p vectorcraft-cli
|
||
claude mcp add vectorcraft -- "$PWD/target/release/vectorcraft-cli" mcp
|
||
# or pin a mode:
|
||
claude mcp add vectorcraft-headless -- "$PWD/target/release/vectorcraft-cli" mcp --headless
|
||
claude mcp add vectorcraft-app -- "$PWD/target/release/vectorcraft-cli" mcp --connect 127.0.0.1:7979
|
||
```
|
||
|
||
Other clients use the same command in their JSON config:
|
||
|
||
```json
|
||
{"mcpServers": {"vectorcraft": {"command": "/abs/path/target/release/vectorcraft-cli", "args": ["mcp"]}}}
|
||
```
|
||
|
||
For a live session, start the app first: `cargo run --release -p vectorcraft -- --control 7979`.
|
||
|
||
## Tools
|
||
|
||
Coordinates are points in document space: y points down, the origin is the first artboard's top-left, and a new
|
||
document is 612 × 792 (US Letter). New objects become the selection. Most commands act on the selection or on
|
||
explicit `ids`.
|
||
|
||
Paint values (`fill`, `stroke`) accept `"#rrggbb"`, `"none"`, `[r,g,b]` (0..1), `{"c","m","y","k"}`, `{"gray"}`,
|
||
or a full `paint.setFill` params object (`{"gradient": …}`, `{"swatch": "name"}`).
|
||
The Fill/Stroke proxy commands run through `run_command`: `paint.invert` and `paint.complement` recolour the active
|
||
proxy keeping each colour's model, `paint.lastColor` / `paint.lastGradient` re-apply the last solid colour or gradient,
|
||
and `paint.recent` returns the recent colours that every paint command (and the eyedropper) feeds.
|
||
`paint.proxies` returns what the proxies show: the fill and stroke, which one is active, and whether the selected
|
||
objects' fills or strokes differ (`fillMixed` / `strokeMixed`, drawn as a "?" proxy).
|
||
|
||
| Tool | Arguments | Notes |
|
||
|---|---|---|
|
||
| `list_commands` | `{filter?, enabledOnly?}` | The command catalogue: id, label, menu, shortcut, params doc, enablement. |
|
||
| `run_command` | `{command, params?}` | Runs any command. Use it for everything without a dedicated tool. |
|
||
| `inspect_document` | `{}` | Artboards, layer tree (ids, kinds, bounds, paint), selection, history, tool. |
|
||
| `inspect_ui` | `{}` | UI state. Remote mode only. |
|
||
| `select_tool` | `{tool}` | `selection`, `directSelection`, `pen`, `rectangle`, `ellipse`, `polygon`, `star`, `lineSegment`, … |
|
||
| `pointer_gesture` | `{events:[{kind,x,y,mods?}], tool?, mods?}` | `kind` is one of `down`, `drag`, `up`, `move`, `doubleclick`. Events go through the same path as the mouse. |
|
||
| `draw_path` | `{points \| d, closed?, fill?, stroke?, strokeWidth?}` | `points` is `[[x,y],…]` or `[{x,y,in?,out?,smooth?},…]`. `d` is SVG path data. |
|
||
| `draw_shape` | `{shape, …geometry, fill?, stroke?, strokeWidth?}` | `rectangle`/`ellipse`: `x,y,width,height` (plus `radius` for corners). `polygon`: `cx,cy,radius,sides`. `star`: `cx,cy,radius1,radius2,points`. `line`: `x1,y1,x2,y2`. |
|
||
| `set_paint` | `{fill?, stroke?, strokeWidth?, ids?}` | Applies to the selection (or `ids`) and becomes the default for new art. |
|
||
| `press_key` | `{key, mods?}` | Remote: a real key event. Headless: runs the command or tool bound to that shortcut, or sends the key to the busy tool. |
|
||
| `type_text` | `{text}` | Remote only. |
|
||
| `invoke_menu` | `{command, params?}` | Invokes a menu item by command id. Includes UI commands such as `view.*` and `window.*` in remote mode. |
|
||
| `open_panel` | `{panel}` | Remote only. |
|
||
| `screenshot` | `{path?, scale?, artboard?, window?}` | Returns MCP image content (`image/png`, base64) plus a text block. Renders the artboard; `window:true` captures the app window (remote only). |
|
||
| `open_file` | `{path}` | Opens any readable file as a new active document: `.vectorcraft`/`.drawcraft`, `.svg`/`.svgz`, `.pdf`/`.ai`, `.ait`, PNG, JPEG, GIF, WebP, TIFF, BMP (an image opens as a document of its pixel size). Templates open as a new untitled document. `run_command document.formats` lists the formats. |
|
||
| `save_file` | `{path?}` | Saves in the native `.vectorcraft` format. |
|
||
| `export` | `{path?, format?, scale?, artboard?, range?, selection?, outlineText?, options?}` | `svg`, `pdf`, `png`, `jpg`, `webp` or `vectorcraft` (the list comes from `document.formats`). When `format` is omitted, it comes from the path's extension. PDF writes one page per artboard: all of them, or `artboard` (0-based) / `range` (`"1-3, 5"`, 1-based); the other formats write one artboard. `options` carries more format options (e.g. `{"quality": 80}` for JPEG). `selection: true` exports the selected objects cropped to their bounds; `outlineText: true` writes SVG text as paths. Template layers are left out, live effects are kept, and exporting `vectorcraft` never changes the document's path. Without `path` the bytes come back as `dataBase64`. Both backends run the same `document.export` call. |
|
||
| `add_text` | `{text, x?, y?, width?, height?, path?, mode?, pathEffect?, size?, font?, color?}` | Point type at (x, y); area type with `width`/`height`; or `path` + `mode` (`area`/`onPath`) to flow text in or along a path, with `pathEffect` (`rainbow`, `skew`, `3dRibbon`, `stairStep`, `gravity`). |
|
||
| `apply_effect` | `{effect?, params?, ids?}` | Appends a live effect. Without `effect`, returns the effect catalogue with parameters and defaults. |
|
||
| `pathfinder` | `{operation, ids?}` | `unite`, `minusFront`, `intersect`, `exclude`, `divide`, `trim`, `merge`, `crop`, `outline`, `minusBack`. For a live version, apply the `pathfinder.*` effect to a group. |
|
||
| `transform` | `{ids?, dx?, dy?, rotate?, scale?, scaleX?, scaleY?, reflect?, shear?, origin?, copy?}` | Runs move, rotate, scale, reflect, shear in that order. With `copy`, the first step duplicates. |
|
||
| `create_graph` | `{type?, x, y, width, height, csv? \| series?, categories?, rows?}` | The nine Illustrator graph types: column, stacked column, bar, stacked bar, line, area, scatter, pie and radar. Edit later with `graph.setData` / `graph.setType` via `run_command`. |
|
||
| `text_wrap` | `{ids?, offset?, invert?, release?}` | Area type below the objects (same layer) flows around them. |
|
||
| `undo` / `redo` | `{}` | |
|
||
|
||
Appearance stacks: an object can carry several fills and strokes (`appearance.addFill`, `appearance.addStroke`), indexed
|
||
in paint order (0 is painted first, the bottom row of the Appearance panel). `paint.setFill`, `paint.setStroke`,
|
||
`stroke.set`, `stroke.setAdvanced`, `paint.editGradient`, `paint.setGradientGeom` and `transparency.set` take
|
||
`item` to edit one of them; `run_command appearance.setActiveItem {"index": n}` makes that row the target of later
|
||
calls that omit `item` (as clicking the row in the Appearance panel does) until the selection changes; the proxy of
|
||
its kind (`paint.proxies`) and the Gradient tool's annotator then show that row.
|
||
`inspect_document` reports it as `paint.appearanceItem`. Live effects take the same `item` to apply to one fill or
|
||
stroke instead of the whole object: `run_command effect.apply {"effect": "path.offsetPath", "item": 0}`, and
|
||
`effect.remove`, `effect.setParams` (`visible` toggles one) and `effect.duplicate` address that item's effects;
|
||
`apply_effect` uses the active item. `effect.list` reports each object's item effects under `applied[].items`.
|
||
|
||
Errors (unknown tool, bad arguments, a disabled or failing command) come back as a normal result with
|
||
`isError: true` and a message. The model can read the message and retry.
|
||
|
||
Swatches go through `run_command`: `swatch.list` lists every swatch with its colour group, kind and colour;
|
||
`swatch.edit {name, newName?, color?, mode?, global?, spot?}` edits one (fills, strokes and text linked to a
|
||
global or spot swatch follow its colour and name, as one undo step); `swatch.delete {names, unlink?}` deletes
|
||
swatches and colour groups in one undo step (art using a deleted global swatch keeps its colour, unlinked).
|
||
`swatch.new {color?, mode?, spot?, global?, group?}` saves a colour (into a colour group with `group`), gradient
|
||
or pattern; `swatch.newGroup {fromArtwork: true, toGlobal?, includeTints?}` makes a colour group of the selected
|
||
art's colours (by default as global swatches the art links to).
|
||
`swatch.move {names, to?, group?}` reorders swatches or moves them into or out of a colour group (as dragging
|
||
them in the Swatches panel does); give colour group names instead to reorder the groups. Dropping a swatch on art
|
||
in the app runs `paint.setFill` (or `paint.setStroke`, the active proxy) with `ids: [the object under the pointer]`.
|
||
The Swatches panel menu's commands: `swatch.addUsedColors {selection?, global?}`, `swatch.unused` (a query: the
|
||
names Select All Unused selects), `swatch.merge {names}` (the first is kept), `swatch.ungroup {name}` and
|
||
`swatch.sortByKind`.
|
||
Document colour mode: `file.new {colorMode: "cmyk"}` starts a CMYK document with CMYK default swatches, and RGB
|
||
colours applied to it (`paint.setFill`/`setStroke` colours and gradient stops, `swatch.new`) are stored as CMYK;
|
||
Gray stays Gray, and `keepModel: true` keeps a colour as given. RGB documents keep colours as given. Harmonies, Edit
|
||
Colors blends, inversions and Recolor Artwork keep each colour's model (a blend between models takes the document's).
|
||
`swatch.new {colors: [...]}` saves several colours as swatches in one undo step. `color.harmony {color, rule, steps?, variation?, amount?}` answers what the Color
|
||
Guide panel shows: the harmony rule's colours, base first, and per colour its row of `2·steps+1` variations
|
||
(shades, cool or muted on the left, tints, warm or vivid on the right).
|
||
|
||
## Resources
|
||
|
||
| URI | Content |
|
||
|---|---|
|
||
| `vectorcraft://document` | `document.inspect` summary (JSON) |
|
||
| `vectorcraft://document/json` | The complete document model (JSON) |
|
||
|
||
## Examples
|
||
|
||
Draw, look, export:
|
||
|
||
```json
|
||
{"name":"draw_shape","arguments":{"shape":"rectangle","x":72,"y":72,"width":200,"height":120,"radius":12,"fill":"#1e88e5","stroke":"none"}}
|
||
{"name":"draw_shape","arguments":{"shape":"star","cx":400,"cy":300,"radius1":90,"radius2":40,"points":5,"fill":"#ffc107","stroke":"#000000","strokeWidth":2}}
|
||
{"name":"draw_path","arguments":{"d":"M72 400 C 150 300 250 500 330 400","stroke":"#e53935","strokeWidth":4,"fill":"none"}}
|
||
{"name":"screenshot","arguments":{"scale":0.5}}
|
||
{"name":"export","arguments":{"path":"/tmp/art.svg"}}
|
||
```
|
||
|
||
Long-tail commands:
|
||
|
||
```json
|
||
{"name":"list_commands","arguments":{"filter":"align"}}
|
||
{"name":"run_command","arguments":{"command":"select.all"}}
|
||
{"name":"run_command","arguments":{"command":"object.align","params":{"align":"left"}}}
|
||
{"name":"run_command","arguments":{"command":"object.group"}}
|
||
```
|
||
|
||
Drive a tool like a mouse:
|
||
|
||
```json
|
||
{"name":"pointer_gesture","arguments":{"tool":"ellipse","events":[
|
||
{"kind":"down","x":100,"y":100},{"kind":"drag","x":150,"y":140},{"kind":"up","x":200,"y":180}]}}
|
||
```
|
||
|
||
Edit a gradient with the Gradient tool's annotator (the bar runs along the vector; stop chips sit 10 px under
|
||
it, midpoint diamonds 7 px above): a click on the bar adds a stop, dragging a chip moves it (drag it off the bar to
|
||
delete it, hold Alt to copy it), dragging the end handle changes the vector. The selected stop
|
||
(`gradient.selectStop`) takes Delete and ←/→:
|
||
|
||
```json
|
||
{"name":"run_command","arguments":{"command":"paint.setFill","params":{"gradient":{"start":[100,150],"end":[200,150]}}}}
|
||
{"name":"pointer_gesture","arguments":{"tool":"gradient","events":[{"kind":"down","x":130,"y":150},{"kind":"up","x":130,"y":150}]}}
|
||
{"name":"pointer_gesture","arguments":{"events":[{"kind":"down","x":130,"y":160},{"kind":"drag","x":160,"y":160},{"kind":"up","x":160,"y":160}]}}
|
||
{"name":"press_key","arguments":{"key":"Delete"}}
|
||
```
|
||
|
||
Freeform gradients: `paint.editGradient {kind: "freeform"}` places four or more colour points inside each selected
|
||
object (coloured along the stops; `mode: "points"|"lines"` is the Draw toggle). `paint.freeform.get` lists the points
|
||
(document coordinates), lines and the selected point; `paint.freeform.addPoint {at, color?, opacity?, spread?,
|
||
line?}`, `setPoint {index?, …}`, `deletePoint {index?}`, `addLine {points}`, `splitLine {line, segment, t?}` and
|
||
`selectPoint {index|null}` edit them (`index` defaults to the selected point; each edit is one undo step). With the
|
||
Gradient tool on a freeform gradient a click on a point selects it (drag to move it), a click on a line adds a point
|
||
on it, a click elsewhere on the art adds a point (in Lines mode joined to the selected one), and Delete removes the
|
||
selected point:
|
||
|
||
```json
|
||
{"name":"run_command","arguments":{"command":"paint.editGradient","params":{"kind":"freeform","mode":"lines"}}}
|
||
{"name":"run_command","arguments":{"command":"paint.freeform.addPoint","params":{"at":[150,150],"color":"#ff3366","spread":20}}}
|
||
{"name":"pointer_gesture","arguments":{"tool":"gradient","events":[{"kind":"down","x":180,"y":170},{"kind":"up","x":180,"y":170}]}}
|
||
```
|
||
|
||
Raw protocol (for debugging):
|
||
|
||
```sh
|
||
printf '%s\n' \
|
||
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"sh","version":"0"}}}' \
|
||
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
|
||
'{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"draw_shape","arguments":{"shape":"ellipse","x":10,"y":10,"width":100,"height":80,"fill":"#ff0000"}}}' \
|
||
'{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"inspect_document","arguments":{}}}' \
|
||
| target/release/vectorcraft-cli mcp --headless
|
||
```
|
||
|
||
## Headless batch CLI
|
||
|
||
```sh
|
||
vectorcraft-cli commands # command catalogue (JSON)
|
||
vectorcraft-cli run --in in.svg \
|
||
--cmd select.all --cmd paint.setFill --params '{"color":"#ff0000"}' \
|
||
--export out.svg --export out.png --scale 2
|
||
vectorcraft-cli run --cmd file.new --params '{"width":800,"height":600}' \
|
||
--cmd shape.star --params '{"cx":400,"cy":300,"radius1":200,"radius2":90}' \
|
||
--export star.vectorcraft
|
||
```
|
||
|
||
`run` prints one JSON line per step (`open`, `cmd`, `export`) and exits non-zero on the first failure. `--params`
|
||
applies to the `--cmd` just before it. `run` also accepts the host commands `file.open`, `file.save`, `file.export`,
|
||
`file.exportForScreens` and `tool.select`. `run --in`, `convert` and `info` read every format `document.open` reads
|
||
(`vectorcraft-cli --help` lists them).
|
||
|
||
## Transparency and opacity masks
|
||
|
||
Transparency and opacity-mask commands take `ids` (or `id`), so they need no selection. Opacity is a percentage
|
||
(0..100) in every command (`transparency.set`, `object.setProps`, `appearance.setItem`). `transparency.info` returns
|
||
the Transparency panel's values, with `null` where the objects differ. Making a mask from one object gives it an
|
||
empty mask and enters mask editing: art drawn then becomes the mask, and the mask commands act on the masked object
|
||
until `transparency.stopEditingOpacityMask`. Saves and exports never include the editing layer.
|
||
|
||
```json
|
||
{"name":"run_command","arguments":{"command":"transparency.set","params":{"ids":[12],"opacity":40,"blend":"Multiply"}}}
|
||
{"name":"run_command","arguments":{"command":"transparency.makeOpacityMask","params":{"ids":[12,15],"invert":true}}}
|
||
{"name":"run_command","arguments":{"command":"transparency.setOpacityMask","params":{"id":12,"clip":false}}}
|
||
{"name":"run_command","arguments":{"command":"transparency.info","params":{"ids":[12,20]}}}
|
||
```
|
||
|
||
Knockout Group has three states: `transparency.set {knockout: "on"|"off"|"neutral"}` (`true` = on, `false` =
|
||
neutral, the default). In a knockout group each child hides what it covers of the children below it; neutral groups
|
||
pass the enclosing group's setting to their children, off groups never knock out. `knockoutShape: true` makes the
|
||
object's opacity and opacity mask scale how much it knocks out. `transparency.togglePageKnockoutGroup` and
|
||
`transparency.togglePageIsolatedBlending` (`{value?}`) treat the whole page as a knockout or isolated group; they are
|
||
saved with the document and undoable, and `transparency.info` reports them. PDF and SVG write knockout groups as
|
||
soft-masked groups with the same look (the PDF export reports a warning).
|
||
|
||
Groups are not isolated unless Isolate Blending is on: a blend mode inside a group with opacity, a blend mode, an
|
||
opacity mask, knockout or a clip reaches the art below the group, on screen and in PDF (a knockout group's elements
|
||
then composite against the art below it). SVG has no non-isolated groups, so there such groups stay isolated.
|
||
|
||
## Clipping masks
|
||
|
||
`object.clippingMask.make` clips the selected objects by the topmost one, which may be a path, a compound path or a
|
||
text object (it loses its paint). Compound holes, even-odd fills, glyph outlines and the union of a group's members
|
||
clip alike on screen, in raster export and in SVG and PDF. `object.clippingMask.release` turns the clip group into a
|
||
plain group and keeps the clipping path, unpainted.
|
||
|
||
```json
|
||
{"name":"run_command","arguments":{"command":"select.set","params":{"ids":[12,15]}}}
|
||
{"name":"run_command","arguments":{"command":"object.clippingMask.make","params":{}}}
|
||
```
|
||
|
||
`layer.clippingMask.toggle {id?}` is the Layers panel's clipping mask button: the top object of the layer `id` (default:
|
||
the one selected group, else the current layer) becomes its clipping path (unpainted, moved to the bottom of the layer,
|
||
so art added later is clipped too); called again it releases the mask. It returns `{clip}`, and the Layers panel
|
||
underlines clipping-path names.
|
||
|
||
## Graphic styles
|
||
|
||
A graphic style holds an appearance (fills, strokes, effects) plus opacity, blend mode, isolate and knockout.
|
||
`graphicStyle.new {name?, id?}` captures an object (a group without its own fills or strokes lends its topmost
|
||
object's, type its characters'). `graphicStyle.apply {name, ids?}` gives the objects the style and links them;
|
||
`add: true` adds the style on top of the existing appearance instead. Linked objects stay linked while they keep the
|
||
style's look: editing their appearance or transparency breaks the link, and `graphicStyle.redefine {name?, id?}`
|
||
updates only the objects still linked. `graphicStyle.list` returns each style with the ids linked to it and the
|
||
style of the first selected object; `select.same.graphicStyle` selects an object's fellow users.
|
||
Styles keep placed gradients relative to the bounds of the object they were made from, and each object a
|
||
style is applied to gets them at the same place relative to its own bounds.
|
||
|
||
```json
|
||
{"name":"run_command","arguments":{"command":"graphicStyle.new","params":{"id":12,"name":"Glow"}}}
|
||
{"name":"run_command","arguments":{"command":"graphicStyle.apply","params":{"name":"Glow","ids":[15,20]}}}
|
||
{"name":"run_command","arguments":{"command":"graphicStyle.redefine","params":{"id":12}}}
|
||
{"name":"run_command","arguments":{"command":"graphicStyle.list","params":{}}}
|
||
```
|
||
|
||
`graphicStyle.merge {names, name?}` adds a style stacking the fills, strokes and effects of two or more styles
|
||
(each on top of the ones before it) with the first one's transparency; `graphicStyle.move {name, to}` reorders the
|
||
list (what dragging a style in the panel does). Override Character Color (`graphicStyle.setOptions
|
||
{overrideCharColor}`, the preference `overrideCharColor`, on by default) makes a style applied to type replace its
|
||
characters' fill and stroke with the style's fills and strokes; off, the characters keep their colour under them.
|
||
|
||
## Editing appearance stacks
|
||
|
||
`effect.move {from, to, fromItem?, toItem?, copy?}` reorders an effect or moves it between the object's effects
|
||
(`null`) and a fill's or stroke's (an item index), as dragging its row in the Appearance panel does; `copy: true` copies
|
||
it (Alt-drag). `appearance.duplicateItem {index, to?}` / `{indices}` and `appearance.removeItem {index | indices}`
|
||
act on several fills/strokes at once, and `appearance.showAllHidden` makes every hidden fill, stroke and effect
|
||
visible again. In remote mode, `effect.dialog {effect, index, item}` opens an applied effect's dialog prefilled
|
||
(`ui.dialog.confirm` runs `effect.setParams`); see the control protocol for the "already applied" question.
|
||
|
||
## Overprint
|
||
|
||
Each fill and stroke (and each character's fill and stroke) can overprint: its inks print over the inks below instead
|
||
of knocking them out. `object.setOverprint {fill?, stroke?, item?, ids?}` sets it (groups set their contents, type its
|
||
characters too; `item` aims at one appearance item) and `attributes.info {ids?}` reads it back (`null` where the
|
||
objects differ). Overprint Preview (`view.overprintPreview`) and Separations Preview show it; rendering approximates
|
||
it by multiplying. Older files that listed Overprint Black objects in the document get the flags on load.
|
||
|
||
```json
|
||
{"name":"run_command","arguments":{"command":"object.setOverprint","params":{"ids":[12],"stroke":true}}}
|
||
{"name":"run_command","arguments":{"command":"attributes.info","params":{"ids":[12]}}}
|
||
{"name":"run_command","arguments":{"command":"view.overprintPreview","params":{"on":true}}}
|
||
```
|
||
|
||
## Edit Colors and Recolor Artwork
|
||
|
||
`edit.colors.invert`, `edit.colors.toCMYK`, `edit.colors.toGrayscale`, `edit.colors.toRGB`, `edit.colors.saturate`,
|
||
`edit.colors.adjustBalance` and `recolor.apply` recolour everything inside the selection (or `ids`) in one undo step:
|
||
fills, strokes, text, gradient stops, gradient-mesh points, embedded images (a recoloured copy of the image; linked
|
||
images are left alone) and the tiles of pattern fills and strokes (a recoloured copy saved as a new pattern swatch,
|
||
e.g. "Dots 2"; the original pattern is untouched). `includeImages: false` / `includePatterns: false` leave images or
|
||
patterns out. The Blend commands also grade gradient meshes, which keep their shading.
|
||
|
||
```json
|
||
{"name":"run_command","arguments":{"command":"recolor.colors","params":{}}}
|
||
{"name":"run_command","arguments":{"command":"recolor.apply","params":{"map":{"#ff0000":"#0055ff"},"includeImages":false}}}
|
||
{"name":"run_command","arguments":{"command":"edit.colors.adjustBalance","params":{"mode":"cmyk","m":-20,"k":10}}}
|
||
```
|
||
|
||
## Group, layer and type appearance
|
||
|
||
Groups and layers carry fills, strokes and effects of their own, as in the reference app: their fills and strokes
|
||
paint every member's geometry, and their effects apply to the members as one piece (one combined drop shadow; a
|
||
Transform or Warp moves or bends the whole group). Their Contents row (type: Characters) is a slot in the stack:
|
||
fills and strokes above it paint over the members (characters), those below under them; new items go above it.
|
||
`appearance.moveItem {"from": "contents", "to": n}` puts the row above the bottom `n` items, and an item move takes
|
||
`contents` to say where the row ends up in the same undo step; `document.node` shows the slot as
|
||
`appearance.contents_index`. The `appearance.*`, `effect.*` and `graphicStyle.apply` commands edit the selected
|
||
objects themselves (a group's own stack; layers through `ids`) or, with `target: "contents"`, the objects inside the
|
||
groups and layers. `appearance.targetContents` selects a group's members (double-clicking the Contents row).
|
||
SVG and PDF export bake a group's own fills, strokes and geometry effects into paths; its raster effects stay a
|
||
filter on the whole group.
|
||
|
||
```json
|
||
{"name":"run_command","arguments":{"command":"appearance.addFill","params":{"ids":[7]}}}
|
||
{"name":"run_command","arguments":{"command":"appearance.moveItem","params":{"ids":[7],"from":"contents","to":1}}}
|
||
{"name":"run_command","arguments":{"command":"effect.apply","params":{"ids":[2],"effect":"stylize.dropShadow"}}}
|
||
{"name":"run_command","arguments":{"command":"appearance.addStroke","params":{"ids":[7],"target":"contents"}}}
|
||
```
|
||
|
||
## Swatch libraries
|
||
|
||
Swatch libraries are read-only sets of swatches computed in code (Web Safe 216, Grays and Neutrals, Earth Tones,
|
||
Skin Tone Ramps, Pastels, Brights, Metallic Gradients, Perceptual Scales, Harmony Sets). `swatch.library.list` lists
|
||
them (`id`, `name`, `category`, `count`) and `swatch.library.get {library}` returns one's swatches and colour groups
|
||
in the `swatch.list` shape (`library` is an id or a name). `swatch.library.add {library, names?}` copies swatches
|
||
into the document as one undo step: a colour group's name brings the whole group, a swatch's name the swatch alone,
|
||
no names the whole library; swatches the document already has (same name and paint) are reported under `existing`
|
||
and not added again. `apply: "fill"|"stroke"` also applies the first one, in the same undo step (what clicking a
|
||
swatch in the library panel does). `swatch.resetDefaults {replace?}` brings back the missing default swatches.
|
||
|
||
```json
|
||
{"name":"run_command","arguments":{"command":"swatch.library.get","params":{"library":"earth-tones"}}}
|
||
{"name":"run_command","arguments":{"command":"swatch.library.add","params":{"library":"earth-tones","names":["Clay"]}}}
|
||
```
|
||
`swatch.library.save {path?, format?: "vcswatches"|"gpl"|"css", names?, name?, user?}` writes the document's swatches
|
||
as a library (`.vcswatches` keeps colour models, global, spot, gradients and colour groups; `.gpl` is 8-bit RGB;
|
||
CSS writes custom properties); without `path` it returns `{data}`, and `user: true` saves into the user library
|
||
folder of the desktop app (listed as category `user`, User Defined). `swatch.library.load {path? | data? |
|
||
dataBase64?, name?}` loads a `.vcswatches` or `.gpl` file, or another document's swatches, as a library to add from.
|
||
|
||
## Strokes on type
|
||
|
||
`stroke.set` without an `item` gives type its characters' stroke: weight, cap, join, miter limit and the dash
|
||
options go to every run (the object keeps no stroke of its own); an `item` still edits an object-level stroke added
|
||
with `appearance.addStroke`, which takes every stroke option (dashes, profile, opacity, blend) like a path's and paints under or over the
|
||
characters as its place relative to the Characters row says. To stroke
|
||
some characters, use `text.setRangeStyle {id, start, end, strokeOptions: {weight?, cap?, join?, miterLimit?, dash?,
|
||
dashOffset?, alignDashes?}}`. `inspect_document` reports each object's stroke as `strokeOptions` (type: its first
|
||
run's) in `stroke.set` terms. `stroke.set` on a group leaves the images and symbol instances in it alone.
|
||
|
||
## Flatten Transparency
|
||
|
||
`object.flattenTransparency {ids?, preset?, …options}` turns transparent art into opaque art that looks the same, in
|
||
one undo step. The targets split into groups of overlapping objects; groups without transparency stay as they are.
|
||
Each other group becomes one group of atomic regions: paths filled with the flat colour the art showed there (over
|
||
white; over nothing, keeping alpha, with `preserveAlpha: true`), plus one image where gradients, patterns, images,
|
||
opacity masks or raster effects reach (clipped to those regions with `clipComplexRegions`, a rectangle without).
|
||
`preset` is `high`, `medium` (the default) or `low`; option keys (`balance` 0–100, `lineArtPpi`, `gradientPpi`,
|
||
`textToOutlines`, `strokesToOutlines`, `clipComplexRegions`, `antiAlias`, `preserveAlpha`, `preserveOverprints`)
|
||
override it, at the top level or in `options`. `balance: 0` rasterizes everything; lower balances rasterize groups
|
||
that split into many regions. The result reports `{ids, vector, rasterized, options}`.
|
||
|
||
```json
|
||
{"name":"run_command","arguments":{"command":"object.flattenTransparency","params":{"ids":[12,15],"preset":"high"}}}
|
||
{"name":"run_command","arguments":{"command":"object.flattenTransparency","params":{"balance":0,"lineArtPpi":150}}}
|
||
|
||
## Pattern editing display
|
||
|
||
In pattern editing mode the tile edge (`pattern.options {showTileEdge}`) and the swatch bounds (`pattern.options
|
||
{showSwatchBounds}`: the part of the tiling the swatch repeats, dashed) are drawn in the preference
|
||
`patternTileEdgeColor`.
|
||
|
||
```json
|
||
{"name":"run_command","arguments":{"command":"pattern.options","params":{"showSwatchBounds":true}}}
|
||
{"name":"run_command","arguments":{"command":"prefs.set","params":{"key":"patternTileEdgeColor","value":"#ff4f4f"}}}
|
||
```
|
||
|
||
The Eyedropper: `appearance.copyFrom {source}` copies what the Eyedropper Options pick up and apply from `source` to the
|
||
selection (`ids`); `reverse: true` copies the selection's attributes onto `source` (Alt-click) and `append: true` adds
|
||
the source's fills and strokes on top of each target's stack (Shift+Alt-click). The options live in the preferences:
|
||
`eyedropper.setOptions {sampleSize?, pickUp?, apply?}` (or `prefs.get`/`prefs.set` with key `eyedropper`) reads and
|
||
sets them; a tree names flags under `appearance` (`transparency`, `fill` `{color, transparency, overprint}`, `stroke`
|
||
`{color, transparency, overprint, weight, cap, join, miter, dash}`), `character` and `paragraph`, and a bool for a
|
||
branch sets all of it. `paint.sampleColor {color}` puts a sampled colour (in its own model) into the active proxy.
|
||
|
||
```json
|
||
{"name":"run_command","arguments":{"command":"eyedropper.setOptions","params":{"pickUp":{"appearance":{"stroke":{"weight":false}}}}}}
|
||
{"name":"run_command","arguments":{"command":"appearance.copyFrom","params":{"source":12,"ids":[7,8]}}}
|
||
```
|
||
|
||
## Targeting layers and moving appearances
|
||
|
||
`layer.target {id}` is the Layers panel's target circle: a layer gets its visible, unlocked art selected and is itself
|
||
the target, so `appearance.*`, `effect.*`, `transparency.*` and the opacity-mask commands without `ids` act on the
|
||
layer (its opacity, its own fills and effects, an opacity mask on the whole layer); a group or object is simply
|
||
selected. `document.inspect` reports it as `target`, and any other selection change ends it.
|
||
`appearance.transfer {source, target, copy?}` is dragging a target circle onto another: the target gets the source's
|
||
fills, strokes, effects and transparency, and the source is cleared unless `copy` (Alt-drag). Dropping a circle on the
|
||
panel's trash is `appearance.clear {ids: [id]}`. Masked objects' names have a dashed underline; while an opacity mask
|
||
is edited the panel lists only an `<Opacity Mask>` entry and the document tab says `(<Opacity Mask>/Opacity Mask)`.
|
||
|
||
```json
|
||
{"name":"run_command","arguments":{"command":"layer.target","params":{"id":2}}}
|
||
{"name":"run_command","arguments":{"command":"transparency.set","params":{"opacity":50}}}
|
||
{"name":"run_command","arguments":{"command":"appearance.transfer","params":{"source":12,"target":2,"copy":true}}}
|
||
```
|
||
|
||
## Expand Appearance
|
||
|
||
`effect.expandAppearance {ids?, target?}` (Object → Expand Appearance, enabled when a selected or targeted object's appearance isn't basic)
|
||
turns appearances into plain objects in one undo step: each visible fill becomes a copy of the path painted by that
|
||
fill alone and each stroke its outline filled with its paint (a brushed stroke: its brush art), grouped in paint order
|
||
under the object's id; each piece takes its fill's or stroke's opacity and blend mode, and the group keeps the
|
||
object's transparency and opacity mask. Geometry effects are baked; raster effects become an embedded image at the
|
||
document's raster effects resolution (shadows and outer glows under the art; a blur, feather or inner glow replaces
|
||
the object with the image). Type with effects or fills and strokes of its own is outlined. A group's or layer's own
|
||
fills and strokes become objects among its members, and its members are expanded too.
|
||
|
||
```json
|
||
{"name":"run_command","arguments":{"command":"effect.expandAppearance","params":{"ids":[12]}}}
|
||
```
|
||
|
||
## Tints of global and spot colours
|
||
|
||
A colour linked to a global or spot swatch has a tint (the reference app's T slider): `paint.setFill {swatch,
|
||
tint?: 0..100}` (and `paint.setStroke`) applies the swatch at that percentage, linked, so `swatch.edit` recolours it
|
||
at its own tint. `paint.proxies` and `document.inspect` show the paint as `{type: "solid", color, swatch, tint}`
|
||
(`tint` 0..1, left out at 100 %). `swatch.new {tint?}` with the current fill (or `swatch`) saves a tint swatch,
|
||
"Name 40%": `swatch.list` reports it with `tintOf` and `tint`, applying it links to its base at that tint, and it
|
||
follows edits of its base. A spot tint prints that percentage of its plate (Separations Preview, PDF Separation
|
||
value). `edit.colors.adjustBalance {tint: -100..100}` (Global mode) shifts the tints of the selection's linked
|
||
colours and leaves the rest alone.
|
||
|
||
```json
|
||
{"name":"run_command","arguments":{"command":"paint.setFill","params":{"swatch":"Ink","tint":40}}}
|
||
{"name":"run_command","arguments":{"command":"edit.colors.adjustBalance","params":{"mode":"global","tint":-20}}}
|
||
```
|
||
|
||
## Linked gradient stops
|
||
|
||
Applying a gradient swatch (`paint.setFill {swatch}`) records it as the gradient's `swatch` (the Swatches panel
|
||
highlights it; new stops drop the link). A gradient stop can link to a global or spot swatch like a solid colour:
|
||
give it `swatch` (and `tint` %, default 100 or a tint swatch's own) instead of `color` in `paint.editGradient
|
||
{stops}` or a `gradient` paint's stops. Editing or deleting the swatch then recolours or unlinks the stop, in art and
|
||
in gradient swatches. A spot stop separates on its plate; a gradient whose stops are all tints of one spot ink (or
|
||
paper white, 0 %) exports to PDF as a Separation shading (the writer has no DeviceN, so a gradient mixing a spot
|
||
ink with other colours is written in process colours, with a warning).
|
||
|
||
```json
|
||
{"name":"run_command","arguments":{"command":"paint.editGradient","params":{"stops":[{"offset":0,"swatch":"Ink"},{"offset":1,"swatch":"Ink","tint":20}]}}}
|
||
```
|