The server lists two names for several calls (command_list and
list_commands, command_run and run_command, doc_inspect and
inspect_document, ui_inspect and inspect_ui, ui_screenshot and
screenshot with window:true), but no description said so, and the two
listing and running pairs take different argument names. Each
description now names its pair and the argument that differs, and undo
and redo name each other.
ui_screenshot took no arguments and returned the window at its full
size, which can be several megapixels on a large display.
ui_screenshot and screenshot {window: true} now take an optional
max_side (1..4096) that scales the returned image down to that many
pixels on its longer side. The reply's text gives the returned size as
width/height and the capture's as capturedWidth/capturedHeight; a file
written to path keeps the full capture. max_side on an artboard render
is an error that points to scale. render_preview reads its max_side
through the same check.
docs/mcp.md describes the pairs and the new argument.
The planar map's `RectIndex` sorted rectangles narrow in x by their left
edge and those narrow in y by their top edge, and scanned the rest for
every lookup. A long diagonal segment is narrow in neither, so a hatch of
diagonals put every line in the scanned list and `Outlines::source_at`
(one lookup per edge) cost a scan of the hatch per edge: quadratic in the
hatch below the 65,536-segment cap, as CodeRabbit found on #3357.
`RectIndex` is now a grid of about one cell per item, each cell listing
the items touching it; a lookup reads the cells around the point. An item
is filed under a footprint of rectangles: its bounds (`new`), or, for a
segment, the bounds of its pieces about a cell long (`with_footprint`,
`segment_footprint`), so a diagonal is filed under the cells along it,
not under every cell its bounds cover. A footprint rectangle covering
more than 64 cells or two rows/columns (a face covering most of the art)
is scanned for every point as before; a thin stripe face is filed under
its column of cells. Cells hold `u32` indices.
Tests: `rect_index_reads_a_diagonal_hatch_only_near_the_point` (2,000
diagonals a tenth of a point apart: a lookup got 1,001 candidates with
the old index, now at most n/4, and still every line through the point;
a hatch of stripe faces is filed, not scanned; a collinear, degenerate
grid). `rect_index_finds_every_rectangle_holding_a_point` keeps the
bounds-only contract.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014gCpjL3yUhaH3jRp7ksufh
A document opened or created between edit.undoGroup.begin and
edit.undoGroup.end opens an undo group of its own with the begun
group's name, so an atomic command_batch that starts with file.new or
document.open is one undo step in the new document. The group opens
after the command that opens the document: when that command also
edits it (a command.batch), those edits stay the command's own undo
step, and a canceled group keeps them, so the journal still replays.
Both commands also run with no document open, and a command.batch that
fails rolls back a begun group. Switching to a document that was
already open does not open a group in it.
The installed-release section of docs/mcp.md said how to register the
server but not how to start an installed app for a live session; the
only start command in the setup steps was the cargo run from a
checkout.
It now gives the command that starts the installed app with
--control 7979 on macOS, Windows and Linux, and says that the
launchers open no control port, that the flagless server checks for
the app once when it starts, and what --connect prints when no app
is listening.
Review follow-up. A function-based shading is read once per import (kept
per shading pattern and inks), and all of them draw on one budget of
200,000 function evaluations; past it a shading comes in as its average
colour, then flat grey, each with a warning that names the cause. A
gradient read leaves enough of the budget to average another shading.
A colour that changes in step with the sampling grid is checked across
the middle of the domain before an axis is chosen, so it isn't read
along the wrong one. The grey fallback warning for other shadings names
only them.
Tests: the same shading on 2,000 paths is read once; 40 distinct
shadings stay within the budget; a hostile file (5,000 fills, 20
shadings with 500-step functions) opens in 0.8 s, 253 s before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PDF import turned every function-based shading (ShadingType 1) into a
flat mid-grey. Chromium writes a CSS repeating-linear-gradient this way,
so a red and white hatch came in as a grey bar.
A function-based shading whose colour changes along one axis of its
domain only now becomes a linear gradient across the domain: stops
sampled along that axis, each hard colour change (found by halving the
interval between two samples) two stops at one offset. One whose colour
changes in two directions comes in as its average colour, with a
warning. Either paints only over the shading's domain.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Flat Logo traces a copy of the image at most 1024 pixels on a side,
averaged down by a whole factor, so a 1025-pixel image is traced at
513 pixels. The trace's warnings did not say so, because they compared
the image only with the decoded raster. For an image over 1500 pixels
they said instead that it is traced more slowly, which Flat Logo does
not do.
The trace result now carries Flat Logo's working size when it averaged
the image down. imageTrace.make and imageTrace.makeAndExpand then name
both sizes in their warnings, and the status bar shows that note, in
place of the slower-trace and megapixel notes. Other modes keep their
warnings. The message has a row in each catalog that translates the
other Image Trace warnings.
Live Paint builds its faces and edges from path outlines alone. Make,
Merge and the Live Paint Bucket put every selected object among the
group's hidden sources, so type, an image, a symbol, a blend or a group
holding one of them vanished from the canvas. They now refuse such a
selection before changing anything ("Live Paint takes paths and compound
paths: convert type, images and other objects to paths first"), as type
and raster images can't go into a Live Paint group directly
(https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/create-live-paint-groups.html).
One predicate, vectorcraft_tools::builder::live_paint_takes (Live Paint
groups, paths, compound paths and groups of them), decides it for Make,
Merge and the Properties panel's Merge Live Paint, which no longer shows
for a selection Merge refuses. The message has an @msg row in every
catalog.
Tests: tests_build::live_paint_refuses_type_and_images_and_changes_nothing
(Merge, Make and the Bucket with type, an image or a group holding type:
refused, document, selection and undo history unchanged; a group of
paths and a path still merge) and
panels::properties::tests::merge_live_paint_is_a_quick_action_for_a_live_paint_group_and_more
(hidden with type selected). Both fail without the change.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Properties panel has the additional contextual controls the public
Properties panel overview lists
(https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/properties-panel-overview.html),
each running the existing command:
- Quick Actions: Blend Options for one selected blend while the Blend
tool is in use (the Blend Options dialog; OK is one undo step), and
Merge Live Paint when a Live Paint group is selected with more objects
(livePaint.merge).
- Appearance: the stroke's Variable Width Profile list (the Control
bar's) with the Width tool or when the stroke has a variable width
(stroke.set {profile}); beside fx, with one effect on the one selected
object (its own, or a fill's or stroke's), its name and a Delete icon
that removes it (effect.remove); with several, the icon opens the
Appearance panel, as the overview describes.
- With Direct Selection and corners of one path selected, Corner and
Corner Radius set those corners' kind and radius
(object.setLiveShape); a live rectangle's and polygon's own rows
already do, so they get no second row.
The overview's Variable Font icon was already in Character. Every label
is already in every catalog.
Tests in panels::properties::tests:
blend_options_is_a_quick_action_for_one_blend_with_the_blend_tool,
merge_live_paint_is_a_quick_action_for_a_live_paint_group_and_more,
the_width_profile_shows_for_a_variable_width_or_the_width_tool,
direct_selected_corners_show_their_kind_and_radius,
one_effect_has_a_delete_icon_in_appearance (shown for the right
selection and tool, hidden otherwise, the right command in one undo
step). Documented in docs/target-app-parity.md, docs/ui-parity.md and
docs/gaps.md.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The tool table in docs/mcp.md listed export and add_text twice. The first
export row omitted the ai format, the per-artboard files of SVG, SVGZ and
EPS, options.useArtboards and options.rect, and said that every format but
PDF writes one artboard. The second add_text row omitted vertical.
Each tool now has one row: export keeps the complete row, and add_text
keeps the row with vertical.
A native save was pretty-printed when the document had at most 2,000
layers and objects and its compact JSON was at most 1 MiB. Pretty
printing puts every coordinate on its own indented line, so traced art
with few paths and many anchors could pass that check and be written at
about five times the size of its compact JSON.
The 1 MiB limit now applies to the pretty-printed document, indented as
the file holds it and in the form the save writes: version 1 anchors,
and placed documents expanded for older versions or Include Linked
Files. Documents whose pretty-printed JSON is over 1 MiB are saved as
compact JSON, even when their compact JSON is under it. The 2,000-object
rule and compressed saves are unchanged. docs/mcp.md describes the rule.
The Image Trace panel had no way to compare a trace with its source
image except changing View and changing it back.
The panel now has an eye button to the right of View, enabled with an
Image Trace object selected. While it is held down, the canvas draws the
selected Image Trace objects with their source image, whatever their
View, and draws their View again on release. The document, its View and
the undo history are unchanged. The renderer takes the objects in
RenderOptions::trace_source, which is also part of the canvas frame key
and the raster-effect cache key.
With type written as text, PDF export drew the hyphen that ends a line
broken inside a word as a path. The file's text had no hyphen where the
canvas draws one, so copied or searched text read "considerations"
where the line shows "con-".
The hyphen is now written as text where the line draws it, as the
character the font gives its glyph to: "-", or U+2010 when the font
gives the glyph to U+2010 and not to "-". It is a text run of its own,
and right-to-left lines keep their /ReversedChars marking. The invisible
text of type without paint holds the hyphens too. Kashidas and list
markers are still drawn as paths.
Docs: docs/mcp.md, docs/target-app-parity.md.
The PencilStraight entry in cursors.toml now says the cursor does not
show while Alt is also held with Alt key toggles to Smooth Tool on, and
that the line follows 45° steps from the Constrain Angle. The parity doc
says the Option/Alt straight line waits for #3959.
With Shift held before a press, the Pencil now shows a new cursor,
PencilStraight: its crosshair with a short straight line where the
closing cursor has its ring. The cursor follows the rule the press uses
to start a straight line, so it shows exactly when the press draws one.
With Alt key toggles to Smooth Tool on and Alt held too, the Smooth
cursor shows as before. Once the press is down, and with the
Paintbrush, Shift leaves the tool's own cursor. Use Precise Cursors
turns the new cursor into the plain crosshair.
Draw straight lines with the Pencil tool describes a straight-line icon
on the Pencil while Shift or Alt is held:
https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/draw-shapes/draw-straight-lines-with-the-pencil-tool.html
Picking the two points of a Linear dimension snapped to anchors and to the
line under the pointer, but showed no guides and ignored the art around it.
The picks now go through the drawing tools' Smart Guides (DrawSnap), hovering
too, so the guides show before each click: a point slides into line with other
objects' anchors and centres, and the second onto the construction guides
through the first. An anchor of the line under the pointer still wins (it
links the dimension to that path), then a guide, then the closest point of the
line (#2432). The guides go once the dimension is being placed.
Test: linear_picks_show_and_snap_to_smart_guides (fails on the old tool, which
showed no overlays while hovering). docs/dimensions.md updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With model "keep" (the default), a row whose new color is in another
color model gives linked art the color converted into the art's model.
That art no longer stays linked to the rewritten group swatch, which
holds the color in its own model, and the reply's note reports the
conversion.
When recolor.apply with a group rewrites a global swatch with another
color than the row of the art linked to it, and that row leaves the art's
color as it is, the art now keeps the row's color and is no longer linked
to the swatch. Before, the link stayed and the group's rewrite gave the
art the swatch's new color.
Art linked to a global swatch of the group that recolor.apply rewrites stays
linked when the swatch takes its row's new color: it shows that color at its
tint, and a later swatch.edit still reaches it. Before, the art took the new
color unlinked. With group, the reply adds relinked, the count of other linked
paints that followed the group's new colors.
With Use Artboards and two or more artboards, EPS export now writes a
central file under the path's own name before the file of each artboard.
The central file contains the whole document, and its bounding box is
the visible art, as in an export without Use Artboards. An export of a
single chosen artboard still writes one file at the path's name, and
Export for Screens still writes one EPS per artboard. When no art shows,
only the artboard files are written.
Docs: the useArtboards params text, the MCP export tool's range text,
docs/mcp.md, docs/control-protocol.md and docs/target-app-parity.md.
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.
Every change in the Image Trace panel traced the selected Image Trace
object again at once, and there was no way to adjust several settings
first and trace once.
The panel now has a Preview checkbox before Trace. It is on by default
and kept with the UI preferences; Reset to Default leaves it. With
Preview off, setting changes wait in the panel until Trace applies them
as one undo step, or until Preview is turned on again. When Preview is
turned on with no Image Trace object selected, the object is traced
with the waiting changes when it is selected again. A change of view
keeps the waiting settings, and selecting another Image Trace object
shows that object's own settings. The control channel's ui.set takes
imageTracePreview.
To check what it kept for a live object, or for effect art made from
live objects, type or symbols, the renderer walked the object in every
frame to hash the symbols, patterns and placed files it shows, and it
looked up each symbol instance it found by name in the list of every
symbol. At 1000 x 1000 pixels, the walk took most of the frame on a
page of 2,000 envelopes around instances of the last 2,000 of 10,000
symbols: 83 ms a frame when every frame walked them.
The renderer now keeps what the walk found with the node, which it
holds weakly, and with the document's symbols and patterns as the walk
read them. A later frame makes the key from what was kept while the
node and the document's symbols and patterns stay the same, and walks
the node again when any of them changes. A walk that finds a placed
file is not kept. An evaluation that the live cache does not keep, such
as a thumbnail's copy, is no longer walked. The same page now takes
30 ms a frame, with the same pixels.
Docs: docs/target-app-parity.md
Brush art leaves out the symbol instances and pattern paints whose art
draws a brushed stroke. The brush cache now records whether a brush's art
or the stroke's paint can show a symbol or a pattern, and drops those
entries when the art of the document's symbols or patterns changes, so an
edit that makes a symbol or pattern start or stop drawing a brushed stroke
draws as a new renderer does.
The parity line gives the median and fastest-frame figures of the
benchmark for both documents it names.
The renderer kept brush art per node id and stroke. The copies drawn
for symbol instances, inline graphics and repeat copies share their
art's ids, so they replaced each other's brush art and it was made
again in every frame. The pieces were drawn as plain nodes, so brush
art that is a live object, such as an envelope that a scatter brush
places, was evaluated again in every frame, for a path on the page too.
The brush cache now also keys brush art by the stroked path, so each
copy keeps its own. Kept pieces are drawn through the renderer's
caches: their stroke outlines, live-effect geometry and live object
evaluations are kept between frames. Each frame draws what a new
renderer draws. On one thread at 1000 x 1000 pixels, 2,000 paths on
the page whose scatter brush art is an envelope took 400 ms a frame on
main and take 94 ms, and the fastest frames of 2,000 instances of a
symbol whose art is a brushed path went from 16 ms to 12 ms, with the
same pixels.
Docs: docs/target-app-parity.md
The title field of document.inspect and the document.open reply is the
document's tab name, which comes from the file name once the document
is saved to or opened from a file. A client had to call file.info or
document.info to read the File Info title, so reading a saved
document's title from these replies gave the file name instead.
Both replies now add fileInfoTitle, the title file.info reports, and
keep title as it was. docs/mcp.md's File Info section describes both.
With --connect, open_file, save_file, export and screenshot {path} sent a
relative path to the app unchanged, and the app resolved it against its
own working directory (/ when started from the Finder). A relative path
then named a different file than it does headless, where the server
resolves it against its own working directory.
The server now makes these paths absolute against its working directory
before it sends them, including export's options.path and the path a
screenshot reply reports. Paths inside run_command and command_batch
params go to the app as given. docs/mcp.md describes the rule.
ui.render, which render_preview and screenshot call in the headless
session and in the app, rendered the artboard with the canvas options:
8-bit compositing and edges smoothed shape by shape. A PNG export
composites in floating point and supersamples edges, so the preview
showed edge colors the exported file does not have, most visibly where
shapes meet edge to edge.
ui.render now renders with the options of a PNG export on white, so
its pixels equal the export's at the same scale. render_preview also
no longer enlarges an artboard smaller than max_side: with the default
max_side of 1024, a 612 x 792 page came back as an interpolated
791 x 1024 image, and it now comes back at 612 x 792. docs/mcp.md and
docs/control-protocol.md describe this.
imageTrace.make and imageTrace.makeAndExpand checked their own keys, but
not the keys inside `params`. A misspelled setting such as `colours` was
dropped, and the trace ran with the preset's value and no error.
A key inside `params` that is not a trace setting is now an error that
names the nearest setting, as for a command's own keys. Errors from
imageTrace.makeAndExpand, including those for `enhanced` and a missing
swatch library, name that command instead of imageTrace.make.
A pixel on the edge of a cut-out image with an alpha of 128 or more was
traced with its own color. When that color was a darkened edge tone, it
took whichever palette color lay nearest, and the edge traced as 1 px
slivers of a color that appears nowhere near it.
In Color and Grayscale, a pixel with an alpha of 128 to 254 next to a
pixel under half opaque now takes a color of the fully opaque pixels
within two pixels of it when its own color is that color or a darker
shade of it, counting only pixels it reaches without crossing a
transparent gap; of several such colors, it takes the one the most of
those pixels have. It keeps its own color when one of those pixels has
it, or when its color is not a shade of any of theirs, so a thin
semi-transparent outline of another color stays as it is. Ties go to the color nearest its own, by its gray
in Grayscale. Opaque images trace as before.
recolor.colors and recolor.reduce were unavailable with nothing selected, so
listing a document's colors needed a select.all first, which also changed the
selection. Both now take ids and read the selection when ids is left out.
With nothing selected they read the objects Select All takes and the objects
inside them: hidden and locked objects are left out with what they hold, also
inside a group, as are the objects of hidden, locked and template layers, and
in isolation mode only the isolated art is read. The selection is left
unchanged. An object named in ids or selected is read with everything it
holds, and an object listed twice, or listed together with a group that holds
it, counts once. recolor.apply still recolors only the selection.
Fitting straightens the pieces of an outline that lie close to a line, and
Snap Curves To Lines straightens more of them. The outline of a single
pixel, or of a region one pixel thick along an edge such as an opaque row
of a mid tone under a light shape, can then have all its pieces on one
line. The trace held a selectable path with no area, for example a path
140 x 0 px with Snap Curves To Lines.
In a Color, Grayscale or Black and White trace with Fills, a fitted
outline or hole whose area is below 1e-6 px^2 is now left out. A path
with no area paints nothing, so the traced result paints the same pixels
as before. Without Fills, the 1 px stroke that outlines each area draws
such a loop, so it is kept. A single closed loop keeps its area as
before.
The Pencil and PencilClose entries in cursors.toml, the PencilClose doc
comment and the closing-cursor line in docs/target-app-parity.md said Alt
held while drawing closes the path. A drag started with Shift or Alt
held draws an open straight line and does not show the closing cursor,
so the texts now say Alt is pressed after the drag starts and name that
exception.
With Alt key toggles to Smooth Tool off, holding Alt when a Pencil drag
starts now draws one straight segment from the press point to the
pointer, at the angle the pointer takes, as Shift does at 45° steps.
Pressed on an end of a selected open path, the line continues the path
from that end. Both ends snap to Smart Guides as a stroke's ends do, and
the line stays open when Alt is still held at the release. Shift and Alt
together keep the 45° steps.
Before, a drag started and released with Alt held drew a closed freehand
path. Alt pressed after the drag has started still closes the path at
the release, and with Alt key toggles to Smooth Tool on, Alt still switches
to the Smooth tool. Before a Pencil press, Alt no longer shows the closing
cursor.
Docs: docs/target-app-parity.md, docs/ui-parity.md.
Create Outlines on a frame of threaded text removed the frame, and the
thread's re-flow then added the frame's text to the story, as it does
for a frame that was deleted. The other frames held that text a second
time while the outlines drew it. Object › Expand, and Expand Appearance
where it outlines type or groups it with a shadow, outer glow or Crop
Marks, did the same.
An outlined frame now leaves its thread with the text it shows, and the
other frames keep the text they hold. When the thread's last frame is
outlined, the text it has no room for goes after the text of the last
frame that stays. When Expand Appearance keeps a frame as type in a
group, the type takes the frame's place in the thread. A thread left
with one frame is no longer a thread, and undo restores the thread.
Docs: docs/mcp.md, docs/target-app-parity.md.
Review of #3924: in a flat area of ` <<(` (CMYK or RGBA 32, 60, 60, 40) in an uncompressed image, each `<<` after white space was read as a dictionary, and each read its string, which never ends, to 64 KiB past it: about 16 K bytes a pixel, so a few hundred bytes of it spent the budget and the file was refused.
Whether a byte in a literal string is escaped doesn't depend on where reading began (the run of `\` before it lies after any `(` before it), so the string from any `(` ends after the first `)` that takes the file-wide count of `(` less `)` below the count at that `(`. Those ends are found for the whole file in three linear passes the first time a string read in skipped text runs past 64 bytes: forward for the final count, back for the `(` whose string never ends, forward pairing the rest. Ends further than 64 bytes are kept (at most one for every 16 bytes of the file; a file built to nest past that is refused); nearer ones are read again. So entering a string costs at most 64 bytes, charged as one. A comment's end of line is kept for every comment read, not only the last.
Review of #3924: every `<<` in a string, comment or stream's data was read as a dictionary of its own, and in a run of `<` (an uncompressed image's flat area of grey 60) every position is one, each read to past the run, so about N²/2 bytes for N `<` and a 1 MiB run refused the file.
The reader starts a dictionary there only where its repair scan tries one (after white space or a dictionary's `>>`) or after an object's `N G obj`: so only those `<<` are read, in a run of `<` only the first. A reading goes on as far as 64 KiB past the last `<<` opened on the way, looks at each nested dictionary's `>>` for a stream and marks each `<<`, so it is not read again; a comment's end of line is kept, as many readings cross the same comment; the `<<<` second-`<` reading in the main pass goes (the reader never starts there). The dictionary text a stream check parses is charged to the budget.
Review of #3919: the scan raised its walk-back floor at every `>>stream`, so one inside a literal string moved the floor past the `<<` of the cross-reference stream around it, and that stream was never looked at. The backward walk also counted a `>>` inside a string, and missed a comment between `>>` and `stream`.
The scan now reads the file forward once, as the reader does: literal strings (nested, escaped), hex strings and comments are skipped, `<<` and `>>` pair on a stack, and a dictionary followed by `stream` is a stream's, whose data is skipped. Because the reader can still start at a `<<` inside skipped text (a cross-reference offset or its repair scan), each such `<<` is read as a dictionary of its own, up to 64 KiB, within a budget of four times the file plus 1 MiB; a crafted file that spends it is refused. The scan stays linear.