`sync_views` matched views to documents by uid only while the number of
documents was unchanged. A `command.batch` (or a recording) that closed a
document fell back to matching by position, so the documents after the closed
one took their neighbours' views, and a move later in the same batch was not
applied to the views. The views are now matched by uid whenever the documents
change: a closed document's view is dropped and a new document gets its saved
view. Closing from the UI no longer removes the view by hand.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The editing copy for other apps now writes linear and radial gradients and
grid patterns as themselves instead of as average colors and cut tiles.
- Gradients the art uses are defined in the setup (`n Bn`, then
`%AI5_BeginGradient` … `Bd`, the stops as `Bs` with their opacity and
midpoints, `BD`), and gradient swatches nothing uses in a non-printing
block. Stops keep gray, CMYK, RGB and spot colors at a tint. A
gradient that isn't a swatch gets a name that no swatch or pattern of
the document has.
- Each fill or stroke with a gradient is an instance around its painting
operator (`Bb`, `Bh`, `Bg`, `Xm`, `Bm`, `Bc`, `BB`), on a compound
path's last subpath and on clipping paths too. `Bg` places the gradient
relative to the path: a linear one by its angle and its start and length
as fractions of the path's extent along it, a radial one by the matrix
that takes the unit circle onto its extent ellipse, its center as
fractions of the path's bounds in that circle's space and its radius as
a multiple of the one the center's distances to the right side and to
the bottom give. In a pattern's tile the placement is mirrored top to
bottom.
- Patterns whose tiles make a grid and hold their art are defined with
`%AI3_BeginPattern` … `E`, the tile's box first, and fills and strokes
paint with `p` and `P`; gradient and pattern swatches are in the palette.
- Freeform gradients, gradients along or across a stroke and gradients
without stops, with more than 256 stops or without extent are written
as their average color, and other patterns as their tiles cut to the
shape, each with a warning. Gradients that dither or mix perceptually
are written as classic ones, stops linked to global process colors as
their colors, and pattern swatches without the placement that moves,
turns or scales their pattern, with warnings.
- Reading: an unpainted box that a pattern's definition marks as its
tile's box (`AIPattern_Editor_Backing_Tile_Rect`) is no longer read as
art, and a linear gradient whose stops all lie at one place keeps each
stop on its side.
The definitions, instances and patterns follow sections 3.4, 3.5 and 14
of the version 7 specification
(http://mercator.elte.hu/~saman/hu/okt/AI7FileFormat.pdf). The stop
opacity, `Bg`'s placement and the marked tile box follow the newer form
that current .ai files carry, which that specification predates.
Each of these is written in the form VectorCraft's .ai reader reads, and
the tests read the written data back through that reader.
Docs: docs/file-format-parity.md.
The editing copy of the art now writes a guide as its path followed by
`(N) *`, or `(n) *` when the path is closed, without the `(N) *` before
the path. The legacy version 7 .ai keeps both, as section 6 of the
version 7 specification shows them
(http://mercator.elte.hu/~saman/hu/okt/AI7FileFormat.pdf).
A ui.pointer request waits, ahead of the requests after it, until the
synthetic input sent before it has been applied. When its caller gave
it up during that wait, the request still waited there, and the
requests behind it waited for up to 1024 queued input events and could
time out.
A given-up pointer request is now dropped at once instead of waiting.
While Select All holds a threaded story, a paragraph whose text starts in one of the
story's locked or hidden frames is no longer converted, and the empty parts of a converted
paragraph in such a frame are no longer restyled. Before, a locked frame whose first
paragraph took in the previous frame's empty last part was converted, and so was a
paragraph that ran on from a locked first frame into an editable one.
After Select All in a frame of threaded text, Remove Line Breaks and
Remove Paragraph Breaks changed only the edited frame, and Convert to
Text over MCP converted only the selected frame. Both now act on the
text of every frame of the story as one undo step.
- addon.cleanup.removeBreaks reads the frames of the story as one text,
so a break at the end of a frame before a word in the next one becomes
a space. The same applies when every frame of a story is selected as
objects.
- text.convertListToText without ids, id or a range converts every
paragraph of the story. Locked and hidden frames of the selected story
are not converted, even when ids names them. When the call converts
the whole story, the story stays selected; before, the call reset the
Type tool's selection to the edited frame.
- A selection inside one frame, and unthreaded type, behave as before.
Docs: docs/mcp.md, docs/target-app-parity.md.
`the_fallback_glyph_of_a_consonant_after_its_vowel_sign_is_reported` expected
the fallback face's cmap glyph for the vowel sign I. On the Ubuntu runner the
fallback face is Noto Sans Devanagari, whose shaping draws the sign with
another glyph (545 instead of 32), so the test failed on `main`. It now checks
that two glyphs are drawn in Ka's cluster, the first is not Ka's and the
second is, and keeps the `text.glyphAlternates` checks.
Fixes#5750
push_num formatted every number with write!("{:.*}"), which runs the
exact decimal float formatter, then scanned and trimmed the text. That
formatter was about 95% of the time to export a path of 200,000
anchors.
At 9 decimals or fewer, push_num now scales the value by 10^decimals
and writes the digits of the rounded integer when that rounding is
certain: the scaled value is below 2^53 and further from a half than
the scaling's rounding error. Ties and larger values still go through
the formatter, so the output is the same byte for byte.
Exporting one path of 200,000 anchors takes a median of 43.4 ms
instead of 167.0 ms, and 20,000 rectangles 81.4 ms instead of 141.4 ms
(Windows 11, release build).
A `backquote` given as a string or a number, on the request or on an
event, was read as absent, so `"true"` moved the object's geometry
instead of its pattern. `pointer_gesture` and `ui.pointer`, headless
and desktop, now answer with an error before any event runs.
Where two open paths lie along the same stretch, the planar map gave
that stretch to one of them only, so a merge or an erase across it left
the other path in place there. Every other open path along an edge now
has its own piece in `lines_under`, under a closed path's outline or
along a line in front. `lines`, `edges` and Live Paint's edges stay one
per stretch, and an Alt-click there still takes the piece in front.
Two seeded tests run 300 random edits each on a long path and a long
compound path: anchors added and deleted at the start, middle and end of a
subpath and where a run of segments ends, subpaths closed and opened, anchors
and handles moved, and in the compound path members added and removed and
subpaths added to a member. The subpaths range from 2 anchors to 3,000, open
and closed. After each edit the kept path's elements, subpaths, bounds and
miter tips equal those of a new `KeptPath`, its runs hold each subpath's
segments, and each run's box and tips equal those measured again.
The index arithmetic in `Sub::spliced`, `Sub::resize` and `KeptPath::update`
uses `checked_sub` and `checked_add` instead of plain subtraction. When the
segments or runs kept for a subpath do not fit its anchors before the edit,
`resize` returns `None` without changing anything, and `update` writes the
subpath's elements again from the new anchors and measures it whole, as it
does for a subpath of fewer than two anchors. A compound path whose kept
members do not pair with the new ones returns false, and the renderer makes
the path again whole.
When anchors of a kept path (a path or compound path of 1,024 anchors or
more) are added or deleted, or a subpath is closed or opened, the next
frame writes again only the path elements from the first anchor that
changed to the last, and the closing segment and ClosePath when an end
anchor changed or the subpath was closed or opened. The elements after
the change move with it. Before, such an edit made the whole path again
and measured it whole.
- The runs of segments now carry their own ends. The runs that held the
segments written become one run, split into runs of 128 when it grows
past 256, and only those runs are measured again, with the miter tips
of the run of the segment with length before each span. When a
subpath is closed or opened and the stroke reaches farther from one
than from the other, all of that subpath's tips are measured again.
- On a subpath that is open before and after the edit, the handle before
the first anchor and the one after the last are not compared, so
Delete Anchor Point, which clears them, writes again only the segments
around the anchor deleted.
- A subpath that has fewer than two anchors before or after the edit is
written again whole. A compound path member that gains or loses
subpaths is made again. When members are added or removed, those
members and every member between the first and the last of them are
made again, and the members before and after them stay.
- An edit that adds or deletes anchors in, closes or opens more than 16
subpaths makes the whole path again, as before.
- An edit that leaves every anchor as it was, such as a new stroke,
leaves the kept path as it is.
The pixels and the cull bounds are the same as with a path made again.
Docs: docs/target-app-parity.md.
Doc comments for the Select add-on commands, the Select by Link Status
dialog body, the Links panel menu and the test helpers (review: docstring
coverage).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Selection Tools add-on: addon.select.byLinkStatus selects the image
objects and placed documents whose link is linked, embedded, missing or
modified (one status or several), as links.list reports it. Optional ids
limit the search, add adds to the selection, and zoom runs the existing
Zoom to Selection in the desktop app. Objects that are hidden, locked, on
a template layer or outside the isolated group are left out and returned
with the reason. Only the selection changes.
Select Linked / Embedded / Missing / Modified are in Object > Plug-ins >
Selection Tools and the Links panel menu. Each is off when no image has
that status, and Shift adds to the selection. Select by Link Status...
lists the images with name, status and format, with a name search, a
status filter, checkboxes with All / None and Zoom to Selection. The
status bar says what was selected and how many were left out, with
plural rows in every catalog.
Tests: engine (statuses, ids, add, zoom, skip reasons, bad params),
command sweep, and UI (message, list filtering, menu items, Links panel
menu, headless dialog frame). Docs: mcp, target-app-parity, ui-parity,
roadmap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Layers and Links panels share one thumbnail thread across documents,
and it ran its jobs in the order they came. Rows of a document left with
a long queue held up the thumbnails of the next document shown.
The thumbnail thread can now drop the jobs it has not started for some
slots. Each frame the Layers and Links panels name the document they
show; when it changes, the queued jobs of every other document are
dropped, and when no document is open, all of them are. Those rows ask
for their thumbnails again when their document shows again, and the
thumbnails already made are kept. The Navigator and the arrange panes do
not use the new call.
With 150 rows of 20,000-anchor paths queued for one document, the 8 rows
of such paths of the next document shown get their thumbnails in 153 ms
instead of 3,265 ms (debug test build, median of three runs).
A move also moves where a stroke's gradient is placed, but the slices of a
gradient along or across the stroke take only its stops and kind. The
moved stroke is now compared with the stroke the slices were made for
with the gradient's placement left out, so a placed gradient no longer
makes its slices again after every move.
A path whose stroke has a gradient along or across it kept its slices under
the node's address, so every nudge made the slices again for the whole path,
although its stroke outline was already reused. The renderer now keeps the key
of a stroke's latest slices under the object's id, the stroke's place and the
tolerance level, and when the object has only moved (same stroke and gradient,
same fill rule, anchors moved by one offset) it draws those slices moved by the
offset, translating only the slices that reach the piece being clipped. Scale,
rotate, an anchor edit or a change to the stroke or its gradient makes the
slices again. The pixels are the same.
docs/mcp.md said that with nothing being dragged, remote press_key c
chose the tool bound to C and f ran the shortcut bound to F. Remote
press_key sends the key event without the letter it types, and the app
runs single-key shortcuts only from the typed letter, so neither runs.
The Arc tool, Selection marquee and grid paragraphs now say what
headless press_key does with these letters and point remote callers to
select_tool or invoke_menu.
In remote mode, press_key queues its key for the next UI pass, while
pointer_gesture drives the tool as soon as the app reads the request. A
release sent right after a key could commit the drag before the key
reached the tool, so C pressed while dragging out an arc left it open.
A ui.pointer request now waits, ahead of the requests after it, until
the synthetic input sent before it has been applied.
Review of #5694 asked for two changes.
Add-on, not a core tool, as Enclosing Lasso is:
- The command is addon.contour.set, in an add-on module of its own
(crates/engine/src/cmd/addons/contour.rs) with its tests beside it.
- The tool is listed under Object > Plug-ins > Contour and under its own
"Contour" heading in All Tools. It has no Basic or Advanced toolbar
slot and no default shortcut. The Contour bar is unchanged.
- The add-on drawer test lists Contour with the other add-on tools.
- Docs (mcp.md, the parity documents, ROADMAP) say add-on and use the
new command name; mcp.md gains a Contour add-on section.
Translations: rows for "Contour", "Contour Tool", "Sharp", "Bake" and
"Turn the Contour into plain paths" in all 26 catalogs ("Round" and
"Square" already had them). Drafted from each catalog's own vocabulary
(for Bake, the verb it uses for Expand Appearance; a word other than the
catalog's Stroke or Path word for the tool's name where they collide);
they are not native-speaker reviewed.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Two fixes from review of #5694, each with a test that fails without it:
- Dragging a selected Contour sent the tool's own corner options, so a
Round or Square Contour came back Sharp and lost its miter limit, and
the Contour bar (which reads the effect) disagreed with the shape. The
press now reads the Contour's own corners and miter limit and every
preview of that drag carries them; a new Contour still takes the tool's
options.
- A drag that never moved the distance (along the edge, or rounded to
nothing by Shift) previewed offset 0 and committed an unchanged copy
and an undo step. Nothing is previewed until the distance leaves where
it started.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Add a Contour tool (Modify group). Dragging away from a selected path or
shape's edge makes a copy of it that carries a live Offset Path effect,
grown by the drag; dragging toward the inside shrinks it. The result is
one undo step and stays editable until baked with Expand Appearance. A
second drag on a selected Contour changes its distance instead of
nesting another effect.
- object.contour: the command behind it (offset, sharp/round/square
corners, miter limit), usable over MCP and the control channel too.
- Contour tool: live preview, Shift rounds to whole points, Escape
cancels, a dashed guide with a white halo shows the outline the shape
had, and a pill shows the distance.
- Contour bar: a floating bar above the selection with the offset box,
Sharp / Round / Square and Bake (Expand Appearance).
- Toolbar slot under Modify (Basic and Advanced) and dc-contour.svg, an
original icon with its ASSETS.md row.
- Tests for the command, the tool's pointer gestures and the bar; the
border-with-a-gap recipe (two Contours, bake, Minus Front) is covered
end to end.
- Docs: beyond-Illustrator row, revision histories and the ROADMAP log.
Beyond Illustrator: a VectorCraft design built on Offset Path. Scores and
estimates unchanged.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The Control bar asks whether Select > Start Global Edit can run every
frame it shows a one-object selection. For a path, compound path or
symbol instance that check built the object's form: a copy of every
anchor and handle, normalized into a unit box. On a 200,000-anchor path
it took about 2.2 to 3.0 ms a frame.
The check now walks the object the same way and measures only the box
of those points, then applies the same size test, so it gives the same
answer for every object without allocating. Starting Global Edit still
builds the forms it compares.
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>