add_text passed `color` to text.create only, and text.createInPath built its own character
style without a colour, so type on a path and path-based area type took the current fill.
text.createInPath now uses the same new-type style as text.create (size, font, style, color,
else the current fill), and add_text sends `color` on both routes.
The MCP test draws a blue rectangle (the current fill), turns it into on-path and area type
with color #ff0000, and reads red back through inspect_document.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
# ROADMAP.md
A graph's data can now hold blank cells: an empty CSV cell, a null in
`rows` (any cell that isn't a finite number), the rest of a short row,
or a row that has only a label.
- Columns, bars and stacked segments leave a blank cell out instead of
drawing a zero-height mark.
- Line and scatter graphs break their lines around it and draw no
marker there; the value axis ignores it.
- Area, pie and radar graphs draw it as 0, which is what it adds to
them.
The help pages don't describe blank cells, so this is our own choice:
a cell with no value has nothing to draw, which also matches how a
series with a missing year reads in a report.
In the file a blank cell keeps a 0 in `rows` and is listed in a new
`blanks` field ([category, series] pairs, written only when there are
any). A version that doesn't know blanks ignores the field and reads
the zeros, so files still open there, and files without blanks are
unchanged.
A number in straight quotes is a label, as the help page on graph
labels says (a year as a category name, for one). The CSV reader used
to strip the quotes first and read it as a value. Label columns are now
decided for the whole column: once any row starts with a label, every
row's first cell is its label, so a row with an empty label no longer
shifts its values one place left. The data window writes a header row,
labels that are empty, read as numbers or have spaces around them in
quotes, and blanks as empty cells, so they come back as they were.
graph.setData {} returns blanks in `rows` as null, so the rows it
returns can be edited and sent back. A label-only row keeps its place
with blank values; a row with neither a value nor a label is dropped,
and a first row that is a label followed by empty cells (`Year,,`) is
still read as a header with no series names.
The data grid is built in full for drawing and for the data window, so
it now stops at 10,000 categories of 256 series, which keeps a file
with one very long row from allocating without bound.
Behaviour (quoted labels): Add graph labels and data sets:
https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/visualize-data/add-graph-labels-and-data-sets.html
vectorcraft-cli mcp and vectorcraft --control take the flags PhotoCraft
takes. While roots are in force, every path a command reads (open, place,
relink, SVG image links, library and profile loads, plug-ins, preset
imports, font searches) must lie inside the read root and every path it
writes (save, export, package, Export for Screens and for Web, print to
file, captures, library saves) inside the write root. Paths are made
absolute, links are followed (a missing file's nearest folder's), and the
result is compared by whole components, ignoring case on Windows; `..`
below a missing folder, dangling links and device names are refused. A
root left out grants none of its access; without the flags nothing
changes.
The checks live in one place, vectorcraft_engine::file_access, consulted
by the engine's file helpers and the app's file services. The headless
server confines its whole process; the app confines control requests,
the frames carrying input they inject and the background saves they
start, leaving the person at the keyboard and the app's own folders
alone. The screenshot tool now has the backend write its file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A run (vectorcraft-cli run, the MCP command_batch tool) had no way to use
an id an earlier step returned. A parameter string that is exactly
$N.path or $last.path now takes that value from step N's result
(0-based), e.g. {"id":"$1.id"}; $$ starts a literal $, and a
reference to nothing fails the step. Objects given to a command are now
used as given: an ids element or id that isn't an object id (or an
unresolved reference) fails instead of the command acting on the
selection. text.setRangeStyle styles the selected type object when no
id is given, as text.setStyle does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Discord feedback: "EPS files are always rasterized: they open as bitmaps, in
other programs as vectors". Every EPS saved by Illustrator 2026 fell back to
its TIFF preview (`gcheck` was unknown), and cairo/Inkscape files stopped at
their first image (`status`), dropped masked images (ImageType 3) and failed
on mesh gradients; the warning didn't say where the program failed.
Fallback never silent:
- The warning names the PostScript error, the operator that raised it and
the named procedures it ran in, innermost first ("a PostScript error
(typecheck in `add`), in `inner` in `outer`"); errors caught by `stopped`
aren't reported. It reaches the status bar, document.open, app.open and
file.open (which now return document.open's result, so MCP's open_file over
the control channel gets it), and `vectorcraft-cli info` ("warnings").
- A file whose PostScript can't be read opens as its TIFF preview, else its
Windows metafile preview (as vectors, scaled to the bounding box), else its
EPSI bitmap. es @msg rows for the new messages.
Interpreter:
- Operators programs probe or use: gcheck/scheck, rootfont,
currentcolorrendering, currenthsbcolor, status, token, read,
bytesavailable, fileposition/setfileposition, resetfile, write*, pdfmark
(marks dropped, as Ghostscript does), execform, nulldevice, strokepath,
pathforall, user paths (ufill/ueofill/ustroke/uappend/upath/setbbox, the
encoded form too), setcachedevice2, findcolorrendering, cache, device and
VM queries, revision; (%stdout) (w) file gives a sink.
- `//name` is replaced by its value when read, inside procedures too.
- Tiling patterns become pattern swatches (their PaintProc drawn in pattern
space, one cell; uncoloured ones in the colour under them; makepattern
copies share one swatch); nested patterns stop at 8 levels.
- Type 3 fonts draw their glyphs (BuildGlyph/BuildChar, widths from
setcachedevice/setcharwidth) as outlines grouped per string, spaced by
ashow/widthshow/awidthshow/xshow/yshow/xyshow/kshow; glyphshow and
stringwidth/cshow use them.
- Shadings of types 1 and 4-7 become gradient meshes (data from arrays,
strings or files, packed or not, flags sharing edges), as fills through a
shading pattern too; sampled functions (type 0) are evaluated;
GradientMesh::coons/triangle are shared with PDF import.
- ImageType 3 masked images, interleaved by sample, by row or in separate
sources; ReusableStreamDecode reads its data when made; Flate/LZW PNG and
TIFF predictors.
- Bounded: DeviceN at 32 inks, sampled functions at 2^24 samples and 32
outputs, 4096 patches a shading, strokepath ignores dashes finer than 1e5
pieces, Type 3 glyphs nest 8 deep (a glyph that shows itself overflowed the
stack before the limit), 64-bit index maths for 32-bit targets.
Tests: a synthetic program per generator (cairo/Inkscape, its Type 3 fonts,
matplotlib, CorelDRAW, Affinity, LibreOffice, Ghostscript eps2write,
Illustrator procsets), meshes and function shadings, masked images, fault
messages, probing operators, metafile/EPSI previews, `//name`, user paths,
hostile patterns/glyphs/shadings (run against main, 12 of the 14 generator
and feature tests fail; CorelDRAW- and LibreOffice-style programs already
read there). The import fuzz vocabulary covers the new operators. Checked
against real files made here with Inkscape 1.4 (cairo) and Illustrator 2026
(its own format and the CS-to-CC compatibility formats; all fell back or
stopped early on main and open as vectors now; its Illustrator 8 format
needs the next commit); those files aren't committed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The inspect_document tool (and the control channel's document.inspect)
take {depth?, childLimit?}, so MCP clients get the skeleton without
going through run_command; the tool description points at the
drill-down path.
- document.find rejects a filter of the wrong type instead of dropping
it (which widened the search); one opt_u64 helper parses limit, depth
and childLimit.
- docs/mcp.md spells out find's matching rules; control-protocol.md
and ROADMAP updated; MCP and engine tests cover the new paths.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The export tool's description, the docs' open_file and export rows and the
CLI's run/convert usage listed only some formats. The CLI usage now prints
the writable extensions next to the readable ones, both generated from the
format table (export_extensions), the export tool points to its generated
format enum, and the docs name EPS, DXF, EMF, WMF, SVGZ and templates.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It ran document.save, which keeps the document's own file and format (or
takes the format the path's extension picks), but described itself as a
native .vectorcraft save. The description now says so, with the formats
generated from SAVE_FORMATS, and points to export for the others.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Holding the brush still keeps Twirl, Pucker and Bloat applying, scaled by the
time held: a new Tool::tick (time supplied by the host: canvas frames, or
`holdMs` on ui.pointer / pointer_gesture events) repeats the last point every
0.1 s, one more dab there. Liquify never touches graphs, meshes, type, symbols,
images, or envelopes, repeats and blends with their contents, nor guides in
selection mode; those under the brush come back in `skipped` with a `warning`
that the status bar shows (UiRequest::Status). A drag applies only the dabs
each new sample adds (the stroke's state is kept between previews), giving the
same result as the whole stroke, so journal replay still reproduces it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Double-clicking a Liquify tool opens its Tool Options (dialog liquifyOptions):
the shared Global Brush Dimensions (Width, Height, Angle, Intensity, Use
Pressure Pen), the tool's options (Detail, Simplify with its checkbox, Twirl
Rate, Complexity, Brush Affects, Horizontal/Vertical) and Show Brush Size.
The canvas captures pen pressure from pen/touch force, ui.pointer and MCP
pointer events take `pressure`, and object.liquify takes [x, y, pressure].
Fixes: LiquifyParams::to_json now sends affectAnchors/In/Out; Alt-drag sizes
the brush from its current size (Shift keeps proportions) instead of
collapsing it. Hint-bar text for the seven tools. Registers the MCP Liquify
tests file added in M8.2.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
document.export {format: "psd"} writes layered bitmaps (8 bits per channel),
written from the format's public specification: RGB, CMYK (ink amounts) or
grayscale, the resolution and an embedded profile, PackBits channels, and the
merged image (the flat render). layers (default true) draws each top-level
layer alone as a pixel layer, cropped to what it paints, with its name,
opacity and blend mode; a background colour becomes a Background layer.
maxEditability turns layers and sublayers into groups and every object into a
layer named as the Layers panel names it (text by its text); clipping, masked,
knockout or appearance-carrying layers stay one layer. hiddenLayers writes
hidden layers hidden instead of leaving them out; layers: false writes a flat
image on white. At most 30000 pixels a side.
Export As offers PSD with the psdOptions dialog (the PNG Options rows, then
Color Model, Flat Image / Write Layers with Maximum Editability and Include
Hidden Layers, Embed ICC Profile). Tests read the files back with the psd
crate (MIT OR Apache-2.0, a dev-dependency).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
document.export {format: "tga"} writes uncompressed true-colour Targa files
through the raster export path (resolution, background, anti-aliasing,
artboard ranges): depth 16 (5-5-5 with a one-bit alpha), 24 (flattened on
white) or 32 (8-bit alpha), rows bottom-up, with the version 2 footer.
Export As offers it with the tgaOptions dialog (Depth).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
document.export {format: "bmp"} writes BMP through the raster export path:
depth 1 (black and white), 4 or 8 (a palette by reduction and dither,
optionally RLE4/RLE8), 16 (5-5-5), 24 or 32 (with alpha, a version 4
header); colorModel rgb or gray; fileFormat windows or os2 (a core header);
flipRows writes top-down rows (a negative height). Impossible combinations
are refused as bad params. Export As offers it with the bmpOptions dialog,
which keeps its choices writable together; BMP moves from read-only to read
and write (tests that used .bmp as the unwritable format now use .dwg).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
document.export {format: "tiff"} writes baseline TIFF through the raster
export path (resolution, background, anti-aliasing, artboard ranges):
colorModel rgb (transparency as unassociated alpha), cmyk (ink amounts,
CMYK colours keep their inks) or gray, lzw (weezl), byteOrder little (II)
or big (MM), embedIcc, and the resolution tags. Export As offers it with
the tiffOptions dialog; TIFF moves from read-only to read and write.
The TIFF writer is shared: EPS previews now use it (and its PackBits)
instead of their own copy, so eps depends on render (layers allow it).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- WebP takes lossless and quality; there is no permissive pure-Rust lossy encoder yet, so
lossless: false writes lossless with a warning (the WebP Options dialog says so).
- document.export {format: "txt", encoding: utf8|utf16, lineEndings: lf|crlf,
selectionOnly}: the stories in stacking order (back to front), a thread once, hidden
objects and template layers left out; UTF-16 is little-endian with a byte order mark.
Export As → Text opens Text Export Options.
- File → Save for Office Documents… (document.exportForOffice {ppi, transparent, artboard,
path}): the artboard as a PNG at 72/150/300 ppi on white or transparent, through a small
dialog in the app.
document.formats' writable list and the MCP export tool's enum gain txt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
document.export {format: "png8" | "gif"} reduces the art to a palette of at most `colors`
(2–256) entries: median cut over a 15-bit histogram refined by k-means (perceptual,
selective or adaptive weighting), or the web-safe, black-and-white or grey palettes; art
with that many colours or fewer keeps them exactly. Dithering is Floyd–Steinberg diffusion,
an 8×8 ordered pattern or noise, with an amount. Pixels under half opacity become one
transparent entry; partly transparent edges blend over the matte. PNG-8 is an indexed PNG
(PLTE, tRNS, 1–8 bits per pixel, Adam7); GIF is a GIF89a (optionally interlaced). Both are in
document.formats, Export As (PNG-8 / GIF Options dialogs) and Export for Screens.
document.formats' writable list and the MCP export tool's format enum gain gif and png8
(their tests list them).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Shipped code no longer unwraps, expects or panics: every one of the 124
unwrap()/expect()/panic!/unreachable! sites outside tests now handles the
failure (Result/Option, ?, let-else, defaults) and reports errors to the user.
- Lints: workspace clippy denies unwrap_used, expect_used, panic, todo,
unimplemented and unreachable; clippy.toml allows them in tests, and
integration tests, examples and testkit opt out explicitly.
- Safety net (vectorcraft_engine::guard): a panic in a top-level command rolls
the document back and returns EngineError::Internal; a panicking tool is
reset; the MCP server answers with an internal error and keeps serving; the
UI frame and each control request are guarded.
- APIs that hid failures now return them: Rendered::to_png/to_jpeg/to_webp
return Result (JPEG/WebP errors used to write empty files),
FontDb::face returns Option, Region::top returns Option.
- Fuzzing: engine/tests/import_fuzz.rs feeds garbage, hostile and mutated SVG
and PDF through import → render → export (30k cases locally, no panics).
- Docs: AGENTS.md/CLAUDE.md rule, docs/development.md "Robustness" section,
README pointer and a PR template checklist.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Edit -> "PDF Presets…"; the clipboard preferences read "Legacy vector
clipboard (no transparency)" / "Legacy vector clipboard" (keys copyAicb and
aicbMode unchanged); the second keyboard shortcut set is "Classic Defaults",
and the old set name is still accepted from saved UI preferences, imported
sets and shortcuts.preset. The MCP server instructions, the apply_effect and
create_graph tool descriptions, the text.createInPath params doc,
docs/mcp.md, the ui-egui crate description and the PDF compatibility docs
no longer name another vendor.
New `cargo xtask brands` (run by `cargo xtask ci`) fails on vendor product
or company names in Rust string literals (labels, params docs, menus,
panels, MCP definitions; comments and test code skipped), Cargo.toml
descriptions and text under docs/ and packaging/; a required legacy alias
is marked `brand-ok`. xtask shares one git file listing between the asset
and brand checks. docs/development.md describes the gate.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>