Paste takes a file copied in a file manager by its flavor, which
file_flavour tells from the file's first 4,096 bytes through
fileio::detect. detect names PDF or PostScript bytes in a file called
.ai or .ait by that extension, and file_flavour gave no flavor for
the format ids ai and ait. So a copied .ai or .ait file did not paste
as its art, while the same bytes in a .pdf or .eps file did.
A copied .ai or .ait file whose bytes are a PDF now pastes through the
PDF flavor (clipboard.importPdf), and one whose bytes are PostScript
pastes through the EPS flavor (clipboard.importEps). Other files
with those extensions paste as before. A .ai saved without its PDF
part also gets the PDF flavor, and Paste answers it with
clipboard.importPdf's error, as it does for the same file named .pdf.
Docs: docs/control-protocol.md, docs/development.md,
docs/target-app-parity.md.
document.pdfInfo and clipboard.importPdf, given a PostScript file (an
EPS, or an .ai in an older PostScript format), failed with "this is a
PostScript file (...), which can't be opened yet: save it as PDF,
PDF-compatible .ai or SVG". document.open and file.place read the same
file through the EPS reader. The error told users to convert a file that
VectorCraft opens.
The error now says the file is not a PDF and that File › Open and
File › Place read it as EPS. The 13 catalogs that carry the message get
the new text.
A PDF page's UserUnit (PDF 1.6) sets the size of its user space unit
in multiples of 1/72 inch. The PDF reader has applied it since #1894, so
a page with /UserUnit 10 and a 36 × 50.4 media box opens at 360 × 504
pt. A .ai whose editing data has its artboards in the page's own units
then no longer matched its PDF part. It opened from the PDF part,
without its layers, its hidden objects and the art outside its
artboards, and with its artboards in a row instead of where the
editing data puts them. document.pdfInfo gave such a page's size in
points and its boxes in the file's numbers.
Editing data whose first artboard has the page's size in the page's
units, and not in points, is now compared with the PDF part scaled
down by the UserUnit. The document read from it is then scaled up by
the UserUnit, with its stroke weights, dashes, effects and pattern
tiles. When the layers can't be read, the artboard layout taken from
the editing data is scaled the same way. document.pdfInfo gives the
boxes in points, as it gives the size.
Docs: docs/mcp.md, docs/target-app-parity.md.
When a .ai file, or an EPS with editing data, opened from its editing
data, the reader converted each embedded 8-bit CMYK image to RGB and
stored it as a PNG, so the document lost the image's ink amounts. When
the same .ai opens from its PDF part, the PDF importer keeps them in a
CMYK TIFF.
The reader now stores those samples with ImageBlob::cmyk_tiff, as the
PDF importer does (crates/pdf/src/import_image.rs). An alpha channel
that hides part of the image becomes the image's opacity mask, a
grayscale image over it, as in PDF import. Grayscale, RGB and 1-bit
images still open as RGBA PNGs.
Docs: docs/file-format-parity.md, docs/target-app-parity.md.
The reference app resets tracking and kerning to 0 with Cmd/Ctrl+Alt+Q.
VectorCraft had no command for it.
type.resetTracking (Reset Tracking and Kerning, default Cmd+Alt+Q) sets
tracking to 0 and kerning to Auto, which the Character panel shows as 0,
in the text the Type tool has selected, or else in all the text of
every selected type object. At a caret it resets only the kerning of
the character before it, where Alt+Left/Right kerns. It is one undo
step. The range logic of type.step moves into a shared type_ranges.
When a call gives ids, type.step and type.resetTracking change all the
text of those type objects even while the Type tool edits text, as
paint.setFill does. Before, type.step changed the Type tool's selected
text instead.
Docs: docs/mcp.md, docs/target-app-parity.md.
object.setBounds read x, y, width and height with f64_or, which falls back
to the current value for anything that isn't a JSON number, so
{"width": "60 mm"} returned ok and changed nothing. object.move,
object.nudge, object.rotate, object.scale (sy) and object.shear did the
same with their optional numbers. A value that is neither a number nor
null is now a BadParams error naming the key, as a missing required
number already is.
The profile is registered in the process's colour registry only, so the
next `vectorcraft-cli run` (or the next launch of the app) doesn't know it
and edit.colorSettings falls back to the built-in profile. The command's
description now says so, and that the profile has to be loaded again in
each run that names it. Keeping profiles across runs would need a place in
the preferences to remember them, which is left for the maintainers.
document.export, document.serialize and document.exportSelection write
a PDF-compatible .ai (format: "ai"), and document.formats lists ai as
writable, but none of their params texts named it. The params text of
document.exportSelection named only png, jpg, webp, svg, svgz and pdf,
although the command writes all 20 writable formats. The description
of the MCP export tool and its row in docs/mcp.md named every format
of the tool's format enum except ai, and the range text and the row
said that only PDF writes several artboards.
The three params texts now list every format their command writes,
and document.export's range and useArtboards texts name .ai beside
PDF. The MCP export tool's description names ai, and its artboard and
range texts say that .ai, like PDF, writes one page per artboard. The
range text and the row also say that SVG, SVGZ and EPS write one file
per chosen artboard, and that rasters, DXF, EMF and WMF do so with
options.useArtboards: true.
Docs: docs/mcp.md (the export tool).
The document.exportForScreens params text listed png, png8, jpg, webp,
gif, svg, svgz and pdf for a row's format, and its settings keys left
out svgz. A row of another format VectorCraft writes, such as eps, was
refused with "Export for Screens writes png, png8, jpg, webp, gif, svg,
svgz or pdf". Rows of tiff, bmp, tga and psd are written too, and the
Export for Screens dialog offers TIFF rows.
The params text, its settings keys and the error now list the 12
formats a row writes: png, png8, jpg, webp, gif, tiff, bmp, tga, psd,
svg, svgz and pdf. Which formats a row accepts does not change.
Export Selection, Asset Export, the Assets tab of Export for Screens
and the rasterized art of CSS Properties cropped each object to its
visual bounds, which leave out live effects. A path with an 8 pt drop
shadow was exported with the shadow cut off at the path's edge, and
a group's own shadow was cut off the same way.
The crop is now the bounds the renderer measures for what it draws
(`vectorcraft_render::effect_bounds`): `painted_bounds` without its
room for brush art. A brushed path is cropped to its visual bounds as
before, and a Shaper group leaves out its transparent source art as
its visual bounds do. `assets.list` reports the same crop. The
renderer measures a shadow's or glow's reach on every side of the
object, so the image also has transparent room on the side away from
an offset shadow.
Docs: docs/mcp.md, docs/target-app-parity.md.
PDF, EPS, EMF/WMF and raster exports, and SVG exports that outline or
embed fonts, warn when type is set in a font family that isn't
available, but file.print and print.preview gave no such warning. A job
in a missing family printed in Source Sans 3 with no warning, and the
Print dialog's Summary showed none.
Both commands now add the exports' warning for the type that prints:
visible type that fits its frame or path, on the layers that Print
Layers picks, where what it paints (its effects applied) lands on the
paper of an artboard or tile that prints, inside its clipping paths.
The type in the symbols, inline graphics, patterns, scatter brushes,
opacity masks and placed documents of the art that prints counts too.
file.print reads the fonts before raster effects and Print as Bitmap
turn type into images, for PDF and PostScript output.
Docs: docs/mcp.md (Print), docs/target-app-parity.md (Fonts).
dataset.select applied a row's values in the order the variables were
defined, and it skips an object inside a hidden group. When a row
showed a group and set an object inside it, and the object's variable
was defined first, the object was skipped while the group was still
hidden: one selection applied the row in part, and selecting the row
again applied the rest. A text variable on type inside the group was
skipped the same way, and a row that hid a group skipped the objects
inside it whose variables were defined after the group's.
dataset.select, dataset.next and dataset.prev now apply the visibility
values that show objects first, outermost object first, then the other
values in definition order, then the visibility values that hide
objects, innermost object first. Of two variables of the same kind on
one object, the one defined last still wins.
Docs: docs/mcp.md.
Binding art in a hidden group or layer failed with "node N is locked",
even when nothing was locked. The error now says whether the object is
locked, is in a locked group or layer, or is in a hidden group or layer.
Make Text Dynamic and Make Visibility Dynamic reported their errors under
`variable.makeDynamic`, which is not a command id. They now name
`variable.makeTextDynamic` or `variable.makeVisibilityDynamic`.
A data set can hold values for only some variables: `dataset.new` and
`dataset.set` store only the values they are given, and selecting the
data set leaves the other variables alone. Update Data Set replaced the
active data set's values with the values the art reports for every
variable. After one Update, a data set that set only a visibility
variable also held the current text of the bound type, and selecting it
again wrote that text back. Update also dropped a variable's value when
its objects were locked or inside a hidden or locked layer or group,
because such objects report no value.
`dataset.update` now writes the art's current value for each variable
the active data set holds, and keeps the data set's value when the art
reports none. A variable that the data set does not hold stays out of
it, including one defined after the data set was captured;
`dataset.update {all: true}` also adds the other variables that the art
reports a value for. The reply adds `left`, the variables it did not
write. The Variables panel's Update Data Set button and menu item run
it without `all`.
Docs: docs/mcp.md, docs/target-app-parity.md.
View › Proof Setup had two color blindness items, protanopia and
deuteranopia. A third item, Color blindness – Tritanopia-type, proofs
the canvas with a tritanopia simulation, and the Separations Preview
panel's Proof Setup list offers it too. `view.proofSetup` takes
`target: "tritanopia"`.
The simulation follows Brettel, Viénot and Mollon (1997): a color
keeps its L and M cone responses, and its S response is replaced so
that it lies on one of two half-planes. Both contain the neutral
axis; one passes through the 485 nm color and the other through the
660 nm color. The neutral axis runs to the display's white, so grays
keep their value. The matrices are computed at
compile time from the published inputs cited in the code. The
protanopia and deuteranopia matrices are unchanged.
A Pencil or Paintbrush stroke always left the path it drew or continued
selected. The reference app's Pencil and Paintbrush options have Keep
selected for this, and VectorCraft's Tool Options had no such row.
The Pencil and Paintbrush Tool Options now have a Keep selected
checkbox, on by default, which keeps today's behavior. When it is off,
a new path is not selected and the selection stays as it was, and a
continued path is removed from the selection while the rest stays. The
stroke is still one undo step. The option persists like the tools'
other options, and path.freehand and brush.freehand take keepSelected.
Docs: docs/target-app-parity.md, docs/ui-parity.md, docs/control-protocol.md.
In a dev build, opening a native file whose area type has 2^60 rows
or 2^60 columns failed with an internal error ("capacity overflow"),
and opening one with 2^32 rows and 2^32 columns failed with "attempt
to multiply with overflow". Text layout reserved room for rows times
columns frame cells. `text.areaOptions` keeps both counts within 1 to
100, but reading a document kept the counts the file gave.
Reading a document now clamps each count to 1 to 100
(`AreaOptions::MAX_ROWS_COLUMNS`): a larger count reads as 100, and 0
reads as 1, as layout already treated it. Text layout clamps both
counts the same way. Area type with 1 to 100 rows and columns lays out
as before.
The cells of a frame now share one copy of the frame's flattened
polygons and one copy of the flattened text wrap shapes. Each cell held
its own copy: in a dev build, a test process that laid out text in 100
rows and 100 columns of a circle frame of radius 1,000,000 pt peaked at
1.33 GB of memory, and now peaks at 100 MB.
Duplicate Artboards, the Artboard tool's Alt-drag (artboard.move with
copy) and Paste of a copied artboard gave the new artboard the largest
artboard id plus one, saturating. In a document with an artboard id
of 4,294,967,295 (u32::MAX), which only a hand-made file has, the copy
got that same id. Artboard guides record the id of their artboard.
Deleting either of the two artboards with that id then deleted the
guides of both, and guide.list reported the copy's guides on the
other one.
push_artboard_copy now takes the id from Document::next_artboard_id,
as artboard.new and Convert to Artboards do. Since the #1718 fix, that
function returns the smallest free id when the largest is u32::MAX.
In a document whose artboard ids are all below u32::MAX, a copy gets
the same id as before.
Docs: docs/target-app-parity.md.
Transform Each had Reflect X and Reflect Y swapped relative to Effect › Distort ›
Transform and the Transform panel. Properties flip buttons also used inverted
tooltips. Align them so Horizontal is left-right and Vertical is top-bottom.
Co-authored-by: Cursor <cursoragent@cursor.com>
With Align to Artboard chosen, the Align panel's six Distribute Objects
buttons and its two Distribute Spacing buttons acted on the selection.
object.distribute and object.distributeSpacing took no to or artboard
param. Distribute Objects kept the outermost objects where they were
and did nothing with two objects, and Distribute Spacing kept the first
object's left or top edge and the last object's right or bottom edge
where they were.
Both commands now take to ("selection", "artboard" or "key") and
artboard, and the panel sends its Align To choice as to. When to is
"artboard", the first object's left or top edge goes to the artboard's
left or top edge, and the last object's right or bottom edge goes to
the artboard's right or bottom edge. Distribute Objects spaces the
other objects evenly by its button's line, and two objects are enough.
Distribute Spacing without a spacing value makes the gaps between the
objects equal. Both commands use the same artboard as object.align.
The app passes the active artboard to both commands, and recorded
actions leave it out, so an action plays on the artboard that is
active when it plays. With "selection" or "key", or without to, both
commands work as before. Distribute Spacing with a spacing value still
keeps the key object in place with each of the three to values. With
anchor points selected, both commands still space the points among
themselves and do not use to.
Docs: docs/mcp.md, docs/target-app-parity.md.
Place flattened every top-level layer into its objects, so an SVG's clipped top-level group lost
its clip and its clipping path came in as a loose, invisible path; sublayers lost their clips the
same way. A clipping layer is now placed as one clip group, for SVG/DXF and for the art placed from
an artboard (PDF, EPS, placed documents) alike.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Type is written as version 7 text objects (§15): point type, area type and type on a path, with
fonts by PostScript name re-encoded to Windows-1252, and each run's paint, size, scaling,
tracking, leading, alignment and indents; type 8-bit text can't hold (or all, with outlineText)
is outlines. The format hides only layers, so hidden objects go to a hidden layer above their
own (hiddenObjects "layer", the default), are left out ("omit") or show ("visible"). The .ai
reader reads legacy type (To … TO, TE, %AI3_BeginEncoding) and ignores Ar; the import fuzzer
gets legacy type tokens.
aiVersion 7 saves through the legacy writer, one file per artboard; range picks the artboards
and document.save writes them again. Appearances are expanded first. Transparent images
(see-through pixels, opacity or a blending mode) are flattened with the art under them into
opaque images clipped to where they change it (transparentImages "flatten", the default), or
left out ("omit"); both warn.
Drag out from a segment, corner, circle or arc to place a dimension; plain clicks pick two points or two lines. Radial dimensions on perfect arcs, picas, Away/Between and Left/Center/Right text placement. The Properties panel edits a selected dimension, with Apply to All (dimension.applyToAll) and Set as Default. Dimension Selected Paths, the Arc/R/Ø prefixes and the custom defaults are left out. The refresh returns at once in documents without dimensions (Document::dimensions); a linked dimension moved on its own keeps its place; modes, units, line types, arrows and text placement are enums; the tool has its own icon.
Use automatic vector smooth shading without a separate control.
Enable Cap by default and keep continuous rim curves and bounded seam joins.
Integrate the shared dialog with the other three 3D effects.
Preserve the writer and reader changes while integrating the current main.
The combined candidate passes all eight local CI gates and the 2,048-case
reader panic verification. Hosted checks remain required before landing.
Refs #1893, #1894.
A set whose entries sit in long, deeply nested sections kept every section's names again for each entry (up to hundreds of megabytes from a 4 MiB file); the names kept over all entries now stop at 8 MiB. Name matching compares character pairs in linear time, so a 20,000-entry set of long names is matched in about 2 s instead of 17 s.
Since #1442 (3acc29f8), a caller outside the app gets an error for a
parameter key that the command's params description doesn't name, and
every_description_parses requires each top-level entry of the params
object to start with a key. The note on query in importMatches.list had
a comma at the top level ("text in the name, target or label"), so the
description didn't parse: the test failed, and the command took any key.
The note is now in parentheses, and the command takes kind and query.
A test checks that shortcutSet.read and the three importMatches
commands take every key they read and refuse a misspelled one.
A shortcut set another app saved as slash-name text (a .kys file whose
text starts with a slash name and "{", such as "/Menus {") opens in
Import Shortcut Set from File › Open, from Edit › Keyboard Shortcuts ›
Import… in the desktop app, or from a drop on the window, whether or
not a document is open. The dialog proposes a target for an entry when
an earlier import kept one for that name, or else when one tool,
command or panel matches the name clearly by its id or by one of its
labels. It decodes the file's key and modifier codes from the codes
earlier imports kept, from the example in the AiCommandPalette README
(/Key 48 for the 0 key, with /Modifiers 0 taken as no modifiers), and
from VectorCraft's default shortcuts where at least two entries vote and
four votes in five agree. The user picks the remaining targets, presses
the key for each unknown key code or marks it No equivalent, and chooses
the modifiers for each unknown modifier code. Apply assigns the ready
rows with the Keyboard Shortcuts rules: a tool takes a single key,
optionally with Shift. It goes through the rows from the last one back
and leaves a row out when a later row it applies has the same shortcut
or the same tool, command or panel. Keyboard Shortcuts then shows the
result as unsaved changes until the user clicks OK. Copy Report copies
the set's name, the rows Apply leaves out and the parts of the file that
weren't read.
- shortcutSet.read is a query command: it reads a set and does not
modify the document or the preferences. It returns each entry with
its section, codes and line, and up to 1,000 skipped tokens, each cut
to 80 characters. It reads at most 4 MiB of text and 20,000 entries,
skips blocks nested more than 16 deep and cuts names at 1,024 bytes.
With a path it reads at most 4 MiB and 4 bytes of the file, and with
--automation-read-root (#832) the path must lie inside the read root.
A file whose text starts with "<", such as an XML shortcut file saved
as UTF-8, fails with "This shortcut file is in a form VectorCraft
can't read."
- importMatches.list, importMatches.set and importMatches.reset read
and change the matches kept with the preferences; resetting the
preferences keeps them. With Remember My Choices on (the default),
Apply keeps the targets and codes the user chose, and for the rows it
assigns, it also keeps the matched targets and the codes decoded by
agreement. A later import proposes them.
- The name matcher compares a name with ids by exact match and shared
words. It compares the name with labels, in English and in the
interface language, by exact match, shared words and character
pairs. It proposes a match only when one tool, command or panel
scores at least 0.6 and at least 0.1 more than the next one.
- When Import Shortcut Set is opened from Keyboard Shortcuts, Cancel,
Escape and ui.dialog.cancel close it and return to Keyboard Shortcuts
as it was (dialogs::PARENT). A document's queued Missing Fonts dialog
(#792) opens after Keyboard Shortcuts closes.
- File › Open has a "Shortcut sets" filter, and in the desktop app
Keyboard Shortcuts' Import… shows .json and .kys files.
Docs: docs/mcp.md, docs/control-protocol.md, docs/development.md,
docs/target-app-parity.md, docs/roadmap.md.
With the Type tool editing, a press with Ctrl (Cmd on the Mac) lends the
Selection tool for the drag. Its drag joined the open typing session, so its
preview started from the document before the typing: the type moved or
scaled and lost what had just been typed. The typing is now kept as its own
undo step before the lent tool takes the press, as Undo already does, and
typing goes on after the drag.
A scatter brush whose art is an instance of a symbol stroked with that brush, or any brush whose
art is painted with a pattern whose tile is stroked with it, drew itself without end: a stack
overflow in a hand-written native file. Brush pieces already strip the brushes of their own
strokes; now what they draw by name that would draw a brushed stroke (a symbol instance, or a
pattern paint, also through the symbols and patterns those use) is left out too, in one place
(`stroke_pieces`, which every renderer and exporter calls with the document).
Probed the rest of item 2 of the depth follow-up with hand-written hostile inputs, kept as tests:
graph designs of deep art and of a symbol holding itself, Affinity groups nested to and past
both of its reader's caps, DXF blocks inserting themselves, EMF/WMF saving states without end,
and plug-in manifests and output nested 30,000 to 100,000 deep. None of them overflowed.
The SVG importer runs on a thread of its own since #1852 (for its stack), and the automation
roots in force on the thread that opened the file didn't hold there: an agent confined to a
read root could have the app read any file an SVG links to. The reads now run in the scope of
the thread that asked.
The decoder's 512 MB limit is on the file's samples; a grey image within it
is four times that once converted to RGBA for the Eyedropper or a recolour,
which the web build can't allocate. The size is now read from the header and
held to the allowance the importers and the renderer use.
The rollback undid the batch's interaction, which holds the art, the selection
and panel state only: a Save As step's new file, format and saved state stayed,
and an Undo step (which ends the interaction) left the art and the history
undone. The batch now keeps the documents as they were and puts them back,
and settles a drag or typing in progress first, as Undo does, instead of
running inside it and keeping its baseline as the batch's step. Files the
steps wrote stay written.
Redo swapped the document and the history under a live interaction, whose
snapshot was still the document from before: the next preview or a cancel
brought that one back while the history recorded the redo, so the redone edit
was lost. Redo now settles the interaction first, the way Undo already did: a
drag that changed something is kept as the newest step (which ends the steps
to redo), one that changed nothing is dropped.