File › Scripts now lists the .js and .jsx files of the Scripts folder next
to the preferences, each named after its file without the extension, with
each subfolder as a submenu, above Check Script… and Other Script…. The
help page describes this listing:
https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/install-and-run-scripts.html
Choosing a listed script runs it through script.run without the warning
Other Script… shows. script.listInstalled returns the same listing, and
ui.runInstalledScript {path} runs one entry.
VectorCraft's own rules: the folder is read off the UI thread when the
app starts, when its window becomes active and when the File menu of the
window's menu bar opens; only names are read; links, junctions, hidden
and system entries are left out and checked again before a run; one read
stops at 2,048 entries, 512 scripts, 128 subfolders, 4 levels or 250 ms
and the menu then notes that some scripts were not listed. Under an
automation read root, script.listInstalled and ui.menu.list leave out a
folder outside the root.
#1183 (LloydNicholson) also proposed a Scripts menu listing a folder.
Measure Type by Glyph Bounds (`typeGlyphBounds`, off by default) is in
Preferences > Type > Glyph Bounds and the Transform panel menu. With it
on, the bounding box, the Transform, Control bar and Properties X, Y, W
and H, `object.setBounds`, the transform tools' reference point and the
Arrange commands measure point type selected on its own by the ink of
its glyphs instead of its em box. A typed W or H scales the live type
until its ink has that size.
- The engine caches each type object's ink in text space with its
layout bounds; the cache is not saved.
- Use Preview Bounds adds the characters' strokes, and type with
geometry effects is measured as drawn. Skewed type is measured by its
outlines.
- Type that draws nothing, area type, type on a path and type inside a
selected group are measured as before. Align and Distribute keep
following Align to Glyph Bounds.
Docs: docs/mcp.md, docs/control-protocol.md, docs/target-app-parity.md,
object.setBounds params text.
File › Scripts › Other Script… picks a .js or .jsx file, shows a warning
that can be turned off in Preferences › General, and runs the script
against the open documents through the `vc` object. The new
`script.run` and `script.stop` commands run and stop a script over MCP.
- A run is one transaction. Each document it changed gets one undo step
named Run Script. An uncaught error, a limit or a stop puts every
document, the paint, the clipboard, menu state and preferences back.
- A command that fails inside a run, or whose result is too large to
reach the script, is undone and throws an error the script can catch.
- Files a command wrote stay on disk and the report names them, also
when the run is stopped from the quit question.
- While a script runs, other commands that change anything fail with a
busy error; MCP reports it as JSON-RPC error -32003.
- In the app the run advances one slice per frame with a Stop button in
the status bar. A run that adds a great many objects asks whether to
continue; Stop and Escape end it.
- Commands that change state outside the documents, such as prefs.*,
plugin.* and file.place.queue, are refused to scripts.
Docs: docs/mcp.md, docs/gaps.md, docs/roadmap.md,
docs/target-app-parity.md.
The Edit > Spelling items have been placeholders with nothing behind them. This adds the
engine half of Check Spelling on craft-proof, the shared spelling engine in craft-libs (at the
revision the other craft-libs crates already pin), so no word list is copied here:
- text.spelling.check {ids?} lists the words the dictionaries don't know in every visible,
unlocked type object (or ids), each run checked in its Character panel Language (English
when it has none; nothing in a language without a dictionary), as {id, start, end, word,
lang}. start/end are byte offsets in the object's text, the ones text.editRange takes, so a
word is changed with text.editRange as one undo step. At most 10,000 words, with `more`.
- text.spelling.suggest {word, max?, lang?} gives the dictionary's suggestions, asked for one
word at a time as craft-proof recommends.
English comes from the public-domain Moby word list built into craft-proof. Its dependencies
(log, miniz_oxide, toml, unicode-normalization) are already in the lockfile. The Check
Spelling dialog, Auto Spell Check and Edit Custom Dictionary follow in later PRs.
Commands that read `id` and otherwise act on the selection, the highlighted
layer row or a new object read it with a lenient helper, so `{"id": "abc"}`,
`true` or `{}` acted on the selection instead. They now read it with
`object_param`, which refuses any value that is not a non-negative integer
with "`id` must be a whole number" under the command's name and changes
nothing; an omitted or null `id` keeps the default.
Commands: object.isolate, layer.locate, layer.releaseToLayers,
graphicStyle.new and redefine, charStyle and paraStyle new, apply and
redefine, image.info, links.info, links.goTo, links.unembed, shaper.release,
shaper.expand (whose errors now carry their own name),
transparency.editOpacityMask, transparency.viewOpacityMask, symbol.edit, the
object.blend.spine commands, path.curvature and the type commands that read
type ranges.
targets() read `id` as an object only when it was a number. Any other
value ("abc", "5", true, {}, [1]) fell through to the selection, so a
command such as css.export, whose `id` the shared check does not read,
acted on the selected objects instead of failing. targets() now reads
`ids` and `id` through named_ids(): a value that is not a non-negative
integer is an error, and an omitted or null `id` still means the
selection. A numeric string is refused, as it already is in `ids`.
effect.apply and appearance.addEffect accept the effect as `id` when
`effect` is left out; they now read their targets with that `id` set to
null, so the effect still applies to the selection as before.
The History panel gets a New Document from State button at its bottom,
which runs the new edit.newDocumentFromState command. The command makes
a new document holding the art of a History panel row ({index?}, the
current state when omitted) and makes it the active document. An index
that names no state is an error and changes nothing.
The new document takes the next Untitled-N name, is marked unsaved, has
nothing selected, no steps to undo or redo and no modified date, and
leaves symbol, opacity-mask and pattern editing; the art drawn in a
pattern's tile is stored in the pattern, as Done stores it. Its links point at the files the
source finds, including files found in the source's folder away from the
links' paths. Those files are looked for only within the folders the
asking caller may read; for a caller without limits, within the folders
the caller that opened the source may read. The source
document, its steps and its saved state stay as they are; typing or a
drag in progress is first kept as a step, as Undo keeps it. While an IME
composes, the command waits, as Undo does.
Docs: docs/mcp.md, docs/target-app-parity.md.
- The Selection tool takes a Shift-press on a selected object's centre point
too: a Shift-drag moves the selection held to 45° steps, and a Shift-click
there takes the object out of the selection, as a Shift-press on the object
does.
- One rotate zone for the Selection tool and the shape tools
(`select::rotate_zone`): a press on a bounding-box handle is not a turn, so
the shape tools' zone matches the Selection tool's classification exactly.
- The shape tools' hover cursor follows the press order (corner and pie widgets,
then the centre point, then the rotate zone), and a move of the selection
shows the Selection tool's feedback: SelectionCopy with Alt, ArrowHollow while
it snaps.
- Engine-level tests for the whole gesture (`crates/engine/src/tests/shapetools.rs`):
repeated previews and one undo step, Alt copy, Escape then release, a turn
about the centre, the corner widget still rounding, and an unfilled shape
moved by the Selection tool.
- docs: evidence in target-app-parity.md's Shapes bullet and the M2 milestone,
and the Move cursor's entry names the copy and snap cursors.
A document tab dragged sideways along the tab strip moves its document to the
place an insertion line marks; the current document stays current. Dragged
above or below the strip, the tab still tears off into a floating window. The
views Window > New Window opened move with their document, and each document
keeps its own zoom, centre and artboard.
The move is the engine command `document.move {from?, to}`, so the control
channel, MCP and recordings reach it, and a replay moves the document before
the steps recorded after it. `sync_views` keeps the views with their documents
by uid when the documents change order.
Behaviour: https://helpx.adobe.com/illustrator/using/customizing-workspace.html
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Edit > Copy and Cut put the selected ruler guides on the clipboard,
with the selected objects or on their own. Pasting puts them on the
layer new art goes to, selected, in the same undo step: with objects
they move as the objects do; alone they keep their places, while Paste
in Place / in Front / in Back put them on the active artboard as they
lay on theirs, and Paste on All Artboards on every artboard.
Object > Expand with Object on now turns each symbol instance in the
selection, including one in a selected group or in a placed document's
art, into its art, as Break Link to Symbol places it. It goes one level
deep: the instances in that art stay instances. The art keeps the
instance's id and place in the stacking order, and Expand stays one
undo step.
- Instances with effects stay instances, and Expand Appearance bakes
them as before. Instances with an effect on a fill or stroke,
instances in perspective, and those in blends, repeats and Intertwine
groups stay instances.
- The art keeps the instance's opacity mask, as Break Link gives it.
- A hidden or locked instance's art stays hidden or locked.
- A placed document in an instance's art is expanded too.
- When the instances' art would make more than 1,000,000 objects, they
stay instances, the limit Break Link uses.
- The Fill and Stroke options (object.expand.info, and the Expand
dialog) count the fills, strokes and gradients in the art of those
instances, with a dynamic instance's own changes applied. The info
reports under symbolArt which of them only that art holds, and the
dialog enables those options only while Object is on.
A double-click on a Radial, Grid or Mirror Repeat with the Selection
tool enters isolation mode on it. There its source art is selected and
edited as usual (a double-click on type in it switches to the Type tool
on that text) and every copy follows, the repeat keeping its settings.
Commands whose `id` or `ids` the shared check in Session::execute does not
read still parsed them with id_param/ids_param, which drop anything that is
not a non-negative integer, and then acted on the selection or the current
layer. `object.blend.make {"ids": 3}` blended the selection,
`object.envelope.makeWithWarp {"ids": [2, "x"]}` warped the selection,
`graph.setData {"id": "2"}` changed the selected graph and
`layer.newSublayer {"parent": "2"}` added a sublayer to the current layer.
- cmd::named_ids and cmd::named_id read `ids`/`id` as targets does: a value
that is not an object id, or an unresolved step reference, is an error.
- The symbol and brush, Live Paint, Shape Builder and Image Trace, blend,
envelope and mesh, perspective, Liquify, Puppet Warp and graph helpers use
them in place of the lenient readers.
- layer.clippingMask.toggle checks `id`, and layer.newSublayer checks
`parent`, with checked_id before falling back to the highlighted row or the
current layer.
run_guarded put back only the active document's art, selection and interaction.
An undo step pushed before the panic stayed (Undo then stepped to a state equal
to the current one) and Redo was lost; other documents a command touched kept
their half-applied edits; the active document was restored by index.
It now snapshots every open document with its history and restores them, and
the active document by uid.
Fixes#3599
People reuse the same gradients across documents and rebuilt them by
hand. The Gradient panel now has a Presets menu: the presets of the
app's built-in gradient libraries, grouped by library (theme), then
the saved ones. Choosing one gives the gradient behind the active
proxy (or a solid paint) the preset's type, stops, method and dither,
keeping its placement, as one undo step, through paint.editGradient.
The panel menu saves the current gradient as a preset (named in a
dialog) and deletes saved ones; Hide/Show Presets toggles the row.
Saved presets are kept with the preferences, next to the other user
preset lists (width profiles, flattener, perspective). Stops linked to
a document swatch keep their colour, unlinked, since a preset is used
in other documents. Built-in presets can't be deleted; names are
unique.
Commands: gradient.presets.list, .apply, .save and .delete. The menu's
rows are drawn only while in view, as its headings are.
Tests: engine tests for list, apply (fill and stroke, placement kept,
one undo step), save (unique names, unlinked swatch stops) and delete;
panel tests for applying from the menu and saving, deleting and
hiding presets from the panel menu, including scrolling the menu to
the saved presets. Docs: gaps G13, target-app and UI parity, ROADMAP.
The reference app's Flare tool rotates the rays as the center is dragged
out, Shift constrains them to their original orientation, and Space
held moves the shape being drawn. VectorCraft's Flare tool drew the
rays in one fixed orientation and ignored Shift and Space.
shape.flare takes rotation (degrees, clockwise), which turns the rays
about the center and leaves the halo, the center and the rings as they
are. While the center is dragged out, the rays turn with the pointer,
as a polygon's first corner does, and a drag straight up leaves them
unturned. Shift keeps them unturned, or turned by General › Constrain
Angle as a polygon drawn with Shift is. Space held during the center
drag or the drag that places the rings' end point moves the flare at
its current size, and the drag goes on from there when Space is let
go, as with the shape tools.
Docs: docs/mcp.md, docs/target-app-parity.md.
While the Type tool edits type, Edit > Undo and Redo (the menu, the
keyboard, the History panel and MCP all run edit.undo and edit.redo)
left the caret at its byte offset, clamped to the text. Typing "abc"
before "XYZ" and pressing Undo left the caret after "XYZ".
The engine now tells the active tool about edit.undo and edit.redo
before it finishes the command. The Type tool keeps the edited text and
a list of the steps that changed it, and puts the caret, with nothing
selected, at the end of the span the step changed: where typed text
began after its Undo, after restored text after the Undo of a deletion,
and after retyped text after Redo. For the steps the tool made, it keeps
where the span ended, so text that repeats the characters next to it
puts the caret at the right end. A step that changes no characters,
such as a formatting change, leaves the caret and the selection as they
are. Typed lists keep their caret positions.
A misspelled parameter key got a did-you-mean suggestion, but a
misspelled command id only got "unknown command `select.al`", through
the CLI, MCP and the control channel alike. Session::execute now names
the nearest command id, using the same matching as parameter keys
(ignoring case), and action.batch does the same for an unknown step. An
id with no near match keeps the plain error.
The matching skips candidates whose length differs by more than the
allowed number of edits, so a very long unknown command id or parameter
key is answered at once instead of being measured against every command
id or accepted key.
The new message has a row in every catalog, and docs/mcp.md's
Parameter keys section shows it.
document.find took only name, kind and text, so removing hairline slivers or
every object in one region needed the whole inspect tree and a filter
written outside the app. It now also takes within and intersects
([x, y, width, height] in document points), min and max width, height,
thickness (the smaller side) and area of the geometric bounds, and min and
max anchor count (a compound path counts its members together). Each given
filter must match, and limits are inclusive.
select: true also selects the matches, leaving out hidden and locked
objects, matches inside another match, and compound path members unless
the compound path is isolated; the reply adds selected. Select > Reselect
repeats the find, and an action being recorded records a find that
selects. The count that select.color, the Select > Same commands and the
Select > Object commands report is now the number of objects selected,
after Lock Guides leaves out guide paths.
A file opened or placed off the UI thread waited while the active tool
reported busy. The Dimension tool also reports busy between its clicks,
so after one click a ready file stayed queued with its spinner until
the dimension was finished or canceled. Files also waited between the
clicks of the Blend tool.
The session now records whether the pointer button is held. A file
waits for a busy tool only while the button is held. A file that
opens between a tool's clicks resets the tool, as a switch of
documents does; a file placed in the active document leaves the
tool's clicks in place. Files still wait while a dialog is open, a
drag is under way or a preview is open, and a tool's preview between
its clicks counts as one (the Flare tool after its center drag, the
Dimension tool's line once the pointer moves after its second click).
path.create read closed only when it drew from anchors. Given path
data, {"d": "M0 0 L10 0 L10 10", "closed": "true"} drew the path and
recorded an undo step, while the same closed with anchors fails with
"`closed` must be true or false". shape.rectangularGrid and
shape.polarGrid read rows, columns, concentric and radial given as text
as the count left out, so {"rows": "3"} drew the default 5 horizontal
dividers instead of 3.
path.create now checks closed before it reads d or anchors, and the
grid commands refuse such a count with "`rows` must be a number, not
"3"". Neither command changes the document or its undo history then.
With d, a closed of true, false or null still leaves the path as its
path data draws it.
Docs: docs/mcp.md, docs/target-app-parity.md.
object.transformEach refused reflectX, reflectY, random and copy given
as text, but read strokes, corners, objects and patterns given as text
as the key left out. With Scale Strokes & Effects on,
{"scaleH": 200, "scaleV": 200, "strokes": "false"} scaled the stroke,
recorded an undo step and kept the params for Transform Again, which
repeated the call. artboard.rearrange, artboard.move and
artboard.duplicate read their patterns option the same way, and
artboard.setProps reads its patterns, strokes and corners options the
same way.
These commands now refuse such a value with "`<key>` must be true or
false" before they change anything. So do the other options and
numbers of artboard.setProps and artboard.move, the art of Duplicate,
Copy and Cut Artboard, the shape commands and path.create, path.close,
path.setHandle, path.moveAnchors, the Pencil, Curvature, Reshape
Segment and Blob Brush options, text.create, text.createInPath,
text.setRangeStyle, text.editRange, text.getRange, text.touchType,
text.areaOptions, type.step, the font size steps,
type.opticalMarginAlignment, type.systemLayout, type.fractionalWidths,
type.ruby, the symbol and swatch commands, the Appearance panel's
commands, the gradient and freeform gradient commands, Shape Builder,
Live Paint, Image Trace, the opacity of object.setProps, the radius,
sides and relativeRadii of object.setLiveShape, the reference point of
object.setBounds and the artboard of Align, Distribute and Distribute
Spacing. They refuse a number given as text with "`<key>` must be a
number, not <value>", and a gradient stop's tint with "stop <n>:
`tint` must be a number". A null still reads as the key left out.
Docs: docs/mcp.md, docs/target-app-parity.md.
Store corner radii as fractions of each corner's max so rounding scales with the shape; expose via Corners dialog and object.setLiveShape relativeRadii.
Co-authored-by: Cursor <cursoragent@cursor.com>
With View › Snap to Grid on, a whole object moved with the Selection,
Group Selection or Direct Selection tool landed on the grid by
whichever of its two edges or its center was nearest a gridline on
each axis. An object on the grid whose width is an odd number of grid
steps has its center half a step between gridlines, so a short drag
could pull it half a step off the grid: on a 20 pt grid, a 100 pt
square at (100, 100) dragged by (8, 8) moved by (10, 10).
A move now lands the nearer of the two edges on a gridline on each
axis, as the reference app's help describes, and the center no longer
snaps. The square above stays at (100, 100). This changes the rule #769
chose for #740. The plain move and the Shift-constrained one both use
grid_pull, so both change.
Docs: docs/target-app-parity.md (Drawing aids).
A click with the Selection tool does not select a hidden or locked
object, an object in a hidden or locked group or layer, or a guide path
while Lock Guides is on, but object.isolate isolated each of them. Once
a locked or hidden group, or a group on a locked or hidden layer, was
isolated, a Selection tool drag moved the member under the pointer. The
Layers panel's Enter Isolation Mode sent the same command for a locked
or hidden layer or group.
object.isolate now refuses a hidden or locked object, or one in a
hidden or locked group or layer, with "the object is hidden or locked"
(the text of layer.target), and a guide path while Lock Guides is on
with "guides are locked". Isolation mode stays off, and the selection
stays as it is.
Docs: docs/mcp.md.
Transform Each, Rasterize, Create Object Mosaic, Create Trim Marks and
Rearrange All Artboards read their optional numbers with f64_or, which
falls back to a default for a value that is not a JSON number.
object.transformEach {"scaleH": "50"} left the selected objects at
their size and reported success. Rasterize did the same with ppi,
Create Object Mosaic did the same with width, height, spacingX and
spacingY, Create Trim Marks did the same with offset, length, bleed and
weight, and Rearrange All Artboards did the same with spacing. Crop
Image cropped to the artboard when rect was a string or another value
that is not an array.
These commands now fail with "`<key>` must be a number, not <value>"
for a value that is not a number or null, as the transform commands
do, and change nothing. Transform Each checks scaleH, scaleV, moveH,
moveV and rotate. Crop Image refuses a rect that is not an array of
four numbers with "rect must be [x, y, width, height]", the message it
already gave for an array of the wrong length; a null rect crops to the
artboard, as a missing one does.
Docs: docs/mcp.md, docs/target-app-parity.md.
anchor_arc_distance measured every segment again for each anchor, so a reshape cost the square of the anchor count; path.reshape now measures the positions once, and the two helpers only it used are gone. The default radius stays half the subpath's bounding-box diagonal, now measured along the path: half its length reached the far corners of a square, moving art the grab is nowhere near (reshape_moves_the_grabbed_point_and_its_neighbourhood).