Skip to content

Commit ba2ee7a

Browse files
authored
Merge pull request storytold#275 from storytold/fix/issue-257
Fix storytold#257: format lists name every format the engine reads and writes
2 parents b743d08 + 926cdc8 commit ba2ee7a

6 files changed

Lines changed: 59 additions & 12 deletions

File tree

‎ROADMAP.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist
8888
- **Automation:**
8989
- Actions panel (record/playback, persisted), generic parameter dialogs for every "…" command.
9090
- JSON-lines control channel with real egui pointer and keyboard injection.
91-
- MCP server with 25 tools (drawing, text, effects, Pathfinder, transforms, graphs, text wrap, export, screenshots, any command), attached to the running app or headless.
91+
- MCP server with 25 tools (drawing, text, effects, Pathfinder, transforms, graphs, text wrap, export, screenshots, any command), attached to the running app or headless. Tool descriptions and replies agree with the engine: format lists come from its format table, `save_file` says it keeps the document's own format, `inspect_document` reports the paint type's characters show, and selection exports report the encoder's warnings.
9292
- MCP protocol: five prompts (`prompts/list` / `prompts/get`: poster, icon set, recolour, trace-and-style, export set), `completion/complete` for prompt arguments and template variables read from the live catalogues (formats, effect ids, command ids, trace presets, swatch and object ids), four resource templates (`vectorcraft://object/{id}`, `//command/{id}`, `//effect/{id}`, `//swatch/{name}`) so an agent reads one thing instead of the whole document, and `logging/setLevel` with `notifications/message`. `docs/mcp.md` has a Protocol section covering the capability table and what is deliberately absent (subscriptions, progress, cancellation, sampling, elicitation, Streamable HTTP — all of which need the transport to interleave reads).
9393
- Headless CLI (`vectorcraft-cli run`, `convert`, `info`, `bench`, `perf`, `mcp`).
9494
- Actions panel that records and plays back commands.

‎apps/vectorcraft-cli/src/main.rs‎

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//!
33
//! ```text
44
//! vectorcraft-cli mcp [--connect 127.0.0.1:7979 | --headless]
5-
//! vectorcraft-cli run [--in FILE] [--cmd id [--params '{json}']]... [--export out.svg|.png|.pdf|.jpg|.webp|.vectorcraft]... [--scale 2]
5+
//! vectorcraft-cli run [--in FILE] [--cmd id [--params '{json}']]... [--export out.svg]... [--scale 2]
66
//! vectorcraft-cli commands
77
//! vectorcraft-cli convert IN OUT [--scale 2] [--artboard 0 | --range 1-3,5] [--outline-text]
88
//! vectorcraft-cli info FILE
@@ -59,16 +59,17 @@ USAGE:
5959
6060
vectorcraft-cli run [--in FILE] [--cmd ID [--params JSON]]... [--export FILE]... [--scale N]
6161
Headless batch: open FILE (any readable format) or start a new document, run commands in
62-
order, export (.svg, .png, .pdf, .jpg, .webp, .vectorcraft by extension). Prints one JSON result per step.
62+
order, export each FILE in the format its extension picks (see Writable formats). Prints one
63+
JSON result per step.
6364
6465
vectorcraft-cli commands
6566
Print the command catalogue as JSON.
6667
6768
vectorcraft-cli convert IN OUT [--scale N] [--artboard I | --range R] [--outline-text]
68-
Open IN (any readable format) and export OUT by extension (.svg, .pdf, .png, .jpg, .webp,
69-
.vectorcraft). --artboard is 0-based, --range 1-based (\"1-3,5\"); a PDF gets every artboard
70-
unless one of them is given, the other formats the first. Live effects are kept;
71-
--outline-text writes SVG text as paths.
69+
Open IN (any readable format) and export OUT in the format its extension picks (see Writable
70+
formats). --artboard is 0-based, --range 1-based (\"1-3,5\"); a PDF gets every artboard
71+
unless one of them is given, EPS the bounds of the art, the other formats the first artboard.
72+
Live effects are kept; --outline-text writes SVG text as paths.
7273
7374
vectorcraft-cli info FILE
7475
Print a JSON summary: title, colour mode, units, artboards, object counts by kind, fonts.
@@ -82,9 +83,10 @@ USAGE:
8283
synthetic N-path document (default 50000). Exits non-zero if a budget is exceeded.
8384
";
8485

85-
/// The usage text plus the formats `document.open` reads.
86+
/// The usage text plus the formats `document.open` reads and `document.export` writes.
8687
fn usage() -> String {
87-
format!("{USAGE}\nReadable formats: .{}\n", vectorcraft_engine::cmd::fileio::OPEN_EXTS.join(", ."))
88+
use vectorcraft_engine::cmd::fileio::{OPEN_EXTS, export_extensions};
89+
format!("{USAGE}\nReadable formats: .{}\nWritable formats: .{}\n", OPEN_EXTS.join(", ."), export_extensions().join(", ."))
8890
}
8991

9092
fn main() -> ExitCode {

‎crates/engine/src/cmd/fileio/mod.rs‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,18 @@ pub const OPEN_EXTS: &[&str] = &[
581581
"eps",
582582
];
583583

584+
/// The extension that picks each writable format when exporting (the format's first; PNG-8 shares
585+
/// `.png` with PNG, so `.png` comes once), in [`FORMATS`] order.
586+
pub fn export_extensions() -> Vec<&'static str> {
587+
let mut v: Vec<&'static str> = Vec::new();
588+
for e in FORMATS.iter().filter(|f| f.write).filter_map(|f| f.extensions.first()) {
589+
if !v.contains(e) {
590+
v.push(e);
591+
}
592+
}
593+
v
594+
}
595+
584596
/// Text files: File → Place sets them as area type (Text Import Options).
585597
pub const TEXT_EXTS: &[&str] = &["txt"];
586598

‎crates/engine/src/cmd/fileio/tests.rs‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,39 @@ fn open_exts_cover_every_readable_format() {
150150
assert_eq!(filters.last(), Some(&("Plug-ins", crate::cmd::plugin::EXTS)), "File › Open installs plug-ins");
151151
}
152152

153+
/// The extensions usage texts list for export: one per writable format, each picking that format
154+
/// (`.png` picks PNG; PNG-8 is asked for by `format`).
155+
#[test]
156+
fn export_extensions_name_every_writable_format() {
157+
let exts = export_extensions();
158+
assert_eq!(
159+
exts,
160+
[
161+
"vectorcraft",
162+
"svg",
163+
"svgz",
164+
"pdf",
165+
"png",
166+
"jpg",
167+
"gif",
168+
"webp",
169+
"tif",
170+
"bmp",
171+
"vctemplate",
172+
"txt",
173+
"dxf",
174+
"eps",
175+
"emf",
176+
"wmf",
177+
"tga",
178+
"psd"
179+
]
180+
);
181+
for e in exts {
182+
assert!(format_for_name(&format!("x.{e}")).is_some_and(|f| f.write), "{e}");
183+
}
184+
}
185+
153186
#[test]
154187
fn formats_query_lists_readers_writers_and_options() {
155188
let mut s = Session::new();

‎crates/mcp/src/tools.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ pub fn tool_definitions() -> Vec<Value> {
275275
tool(
276276
"export",
277277
"Export",
278-
"Export the active document with the engine's document.export (the same bytes in the app and headless): svg (artboard viewBox), pdf (one page per artboard: all, or `artboard` / `range`), png/jpg/webp/gif/png8/tiff/bmp/tga/psd (one rendered artboard; png8 is an indexed .png; psd keeps the layers), txt (the stories, back to front) or vectorcraft (a native copy; the document keeps its path). `selection: true` exports only the selected objects, cropped to their bounds. Template layers are left out; live effects are kept (geometry baked, SVG filters for shadows/glows/blur). Without `path` the bytes come back as dataBase64.",
278+
"Export the active document with the engine's document.export (the same bytes in the app and headless), in any format of the `format` enum: vectors (svg/svgz with the artboard as viewBox, eps, dxf, emf, wmf), pdf (one page per artboard: all, or `artboard` / `range`), rasters (one rendered artboard: png, jpg, webp, gif, png8 as an indexed .png, tiff, bmp, tga, psd with its layers), txt (the stories, back to front), vectorcraft or template (a native copy or template; the document keeps its path). run_command document.formats lists each format's options. `selection: true` exports only the selected objects, cropped to their bounds. Template layers are left out; live effects are kept (geometry baked, SVG filters for shadows/glows/blur). Without `path` the bytes come back as dataBase64.",
279279
obj(
280280
json!({
281281
"format": {"type": "string", "enum": FORMATS.iter().filter(|f| f.write).map(|f| f.id).collect::<Vec<_>>(), "description": "Default: from the path's extension"},

‎docs/mcp.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,9 +159,9 @@ objects' fills or strokes differ (`fillMixed` / `strokeMixed`, drawn as a "?" pr
159159
| `invoke_menu` | `{command, params?}` | Invokes a menu item by command id. Includes UI commands such as `view.*` and `window.*` in remote mode. |
160160
| `open_panel` | `{panel}` | Remote only. |
161161
| `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). |
162-
| `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. PDF, `.ai` and SVG files saved with Preserve Editing reopen as the document they carry. `run_command document.formats` lists the formats. |
162+
| `open_file` | `{path}` | Opens any readable file as a new active document: `.vectorcraft`/`.drawcraft`, `.vctemplate`, `.svg`/`.svgz`, `.pdf`/`.ai`, `.ait`, `.eps`, `.dxf`, `.emf`, `.wmf`, PNG, JPEG, GIF, WebP, TIFF, BMP (an image opens as a document of its pixel size). Templates (`.vctemplate`, `.ait`) open as a new untitled document. PDF, `.ai` and SVG files saved with Preserve Editing reopen as the document they carry. `run_command document.formats` lists the formats. |
163163
| `save_file` | `{path?}` | Runs `document.save`: the document's own file in its own format (native `.vectorcraft` unless it was opened from or saved as SVG, PDF or a restorable `.ai`; then `warnings` say what that format loses). A path's extension picks the format (`.vectorcraft`, `.vctemplate`, `.pdf`, `.svg`, `.svgz`, `.ai`: a PDF carrying the native document, which reopens editable). |
164-
| `export` | `{path?, format?, scale?, artboard?, range?, selection?, outlineText?, options?}` | `svg`, `pdf`, `png`, `jpg`, `webp`, `gif`, `png8` (an indexed `.png`), `tiff`, `bmp`, `tga`, `psd` (layered), `txt` (the document's text) 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 (the reply adds their `bounds`, and reports `format` and the encoder's `warnings` as a whole-document export does); `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. |
164+
| `export` | `{path?, format?, scale?, artboard?, range?, selection?, outlineText?, options?}` | `svg`, `svgz`, `pdf`, `eps`, `dxf`, `emf`, `wmf`, `png`, `jpg`, `webp`, `gif`, `png8` (an indexed `.png`), `tiff`, `bmp`, `tga`, `psd` (layered), `txt` (the document's text), `vectorcraft` or `template` (a native template). The tool's `format` enum and `document.formats` list them, generated from the engine's format table. 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 (the reply adds their `bounds`, and reports `format` and the encoder's `warnings` as a whole-document export does); `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. |
165165
| `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`). |
166166
| `apply_effect` | `{effect?, params?, ids?}` | Appends a live effect. Without `effect`, returns the effect catalogue with parameters and defaults. |
167167
| `pathfinder` | `{operation, ids?}` | `unite`, `minusFront`, `intersect`, `exclude`, `divide`, `trim`, `merge`, `crop`, `outline`, `minusBack`. For a live version, apply the `pathfinder.*` effect to a group (`effect.apply` groups several loose selected objects first). |

0 commit comments

Comments
 (0)