Enter and Escape went to the active tool whatever modifiers were held,
and the command bound to the chord then ran as well. With a command
bound to Cmd+Enter, Cmd+Enter ended the Pen tool's path, opened the
Move dialog with the Selection tool, or typed a line break while the
Type tool edited text, and then ran the command. With a command bound
to Cmd+Escape, Cmd+Escape ended the Pen path, the Type tool's text
editing or isolation mode before the command ran.
A press of Enter or Escape whose chord a command is bound to, matched as
the shortcut dispatcher matches it, now runs only that command, also
when the tool claims the key. While the Type tool edits text, only Cmd
chords count. While a tool is in the middle of an interaction (a drag,
or between the clicks of a tool such as Blend or Dimension), and for
plain Enter and Escape and chords that no command is bound to, the key
goes to the tool as before.
A PDF or .ai page placed linked is a placed document, and File ›
Package listed only the fonts of the document's own type, so the fonts
of the page's type were left out of Fonts/ where an embedded page's
were copied. Package now reads each linked page a document places and
adds the fonts of its type, also when another document placing the same
file copied it first, and also with copyLinks off.
The Place dialog classed every .pdf, .ai and .ait path as a document
that Link places as one locked object. A .ai saved without PDF
compatibility is PostScript, and file.place embeds it as editable art
with Link on or off, so the tooltip was wrong for it. The dialog now
reads the file's header with the same PostScript check file.place uses,
and describes such a file as it does other files Link does not keep
linked.
Turkish is now a complete-message language, so the message file.place
gives for a file that is not a VectorCraft document, a PDF or a
PDF-compatible .ai file needs a tr row.
The File Handling preference Link Placed PDF and .ai Files
(linkPlacedPdf) is removed. File › Place with Link now places a page of
a PDF, .ai or .ait file read as PDF pages as a linked placed document,
as it links a raster image, and link: false embeds the page. Link is on
by default in the Place dialog and in file.place, so a PDF or .ai page
placed from a file, which used to be embedded, is now linked unless
Link is off. A password or textAs: "outlines" still embeds, and so do
EPS and PostScript .ai files.
The Place dialog's Link tooltip describes a PDF or .ai file as a
document.
A .ai file read from its editing data (its PDF part only a placeholder)
finds the files its art places by name, beside it. Package now copies
those files into the same folder as the .ai file, and warns when one has
to be copied under another name.
File › Place with Link kept a link only to raster images and VectorCraft
documents. A PDF, .ai or .ait page was always embedded as a group, so
Update Links never saw a newer version of the file.
A new preference, File Handling › Link Placed PDF and .ai Files
(linkPlacedPdf), is off by default. With it on, Link places the page as
a placed document, as it places a VectorCraft document: the object
keeps the file's bytes, a preview, the page number and the page's box
(crop, art, trim, bleed or media, stored as page_box). Links check,
update and relink it, Break Link and Object › Expand give the group
placing it without Link gives, every output draws the page's vectors,
and saving and reopening keep the link. With the preference off, or
with a password or textAs: "outlines", the page is embedded as before.
EPS and PostScript .ai files are still embedded.
Package copies a placed PDF or .ai file as it is. The Place dialog's
Link tooltip describes a PDF or .ai file as a document while the
preference is on. Relinking a placed document to a file that is not a
VectorCraft document, a PDF or a PDF-compatible .ai file says so.
Docs: docs/mcp.md, docs/target-app-parity.md.
Review follow-up (CodeRabbit): a line that wasn't valid JSON was reported
by its line only, while every other --commands error names the run's step.
It now says `step N (line L)`, N counted with the --cmd steps before it.
run_refuses_malformed_commands_naming_the_step expects `step 2 (line 3)`;
it fails without the change.
Review follow-up (CodeRabbit): the two older G2 bullets counting 46 and 49
filters were superseded by Conté Crayon (57/57) and are folded into one
current bullet; the G2 row in the ranked table and the entry's estimate now
both say 4–6 h, as ROADMAP.md and roadmap.md do. ROADMAP.md's revision
history gains its 2026-10-22 row.
Review follow-up (CodeRabbit): the blank-name check ran on the raw name, so
a name made only of characters XML can't hold (control characters) wrote an
empty <title>. The name is sanitised first, then the fallback applies. The
title test covers such a name; it fails without the change.
The SVG writer always wrote the document's title in <title>
(crates/svg/src/export.rs), so every file of Export As with Use
Artboards, Export for Screens or an `artboard`/`range` export was titled
after the document ("Icons"), whatever its artboard was called. The
writer's `artboard` option can't tell those apart from Save: Save writes
the first artboard too, standing for the whole document.
ExportOptions has a new `artboard_title` (set by the caller, as
`artboard` is, not deserialized): the file is that one artboard, exported
on its own, and its <title> is the artboard's name (XML-escaped as
before), or the document's title when the artboard has no name. The
engine's SVG encoder (fileio/svg.rs) sets it exactly when the export
names its artboards, which is also when a file holds only that
artboard's art (#550): Export As per artboard, document.export /
serialize with `artboard` or `range`, MCP export, and Export for Screens'
artboards, assets, layers and selections (each written as an artboard
named after it). Save and Save As keep the document's title. Export for
Screens' Full Document draws the art bounds as one artboard, which is now
named after the document, so its SVG keeps the document's title too.
docs/file-format-parity.md's SVG row says so.
Tests: title_control_chars::an_artboard_exported_alone_is_titled_after_it
(crates/svg: two named artboards, the second named with markup;
exporting either alone gives its escaped name, the default and an
artboard without artboard_title give the document's title, an unnamed
artboard falls back to it) and
tests_svgenc::an_svg_of_one_artboard_is_titled_after_it (the engine:
`artboard` 0 and 1, useArtboards range all, Save's SVG, Export for
Screens per artboard, an asset and Full Document). Both fail without the
change (the title was "Icons") and pass with it.
Review follow-up (CodeRabbit): step_value clamped once after all of a
frame's steps, so at the lower bound a step down then a step up in the
same frame left the field at the bound. Each step is now clamped as it is
applied. The Stroke panel test steps down and up in one frame at each
bound and expects one step above it; it fails without the change.
Wheel notches and the arrow keys over a focused number field step the
value its text shows (widgets::step_value), and nothing there knew the
field's bounds: the Stroke panel's Weight went on to -3 pt and its Limit
to 0 x in the field, while the panel only clamped what it applied (the
spinner's min, Limit's clamp to 1..500), so the next step went on from
the out-of-range text. A label scrub (scrub::field) had the same gap.
step_value and scrub::field now take the field's bounds and stop at
them, so the field shows where they stopped and the value applied is
the same. The fields that have bounds pass them:
- spin_field / spin_field_auto / spin_plain: their `min` (the spinner
and a typed value already stopped there);
- range_field: its range; it now also takes a value that may be blank
(None), as mixed_field does;
- opt_field (dash and gap lengths): 0;
- the Revolve dialog's values: their range.
Fields that clamped only what they applied now say their range with
range_field instead: the Stroke panel's Limit (1..500), the Transparency
and Properties opacity (0..100), the Transform panel's polygon sides,
and the dialogs' bounded counts, percentages and lengths (Blend Options,
Envelope, Flattener, Liquify, Freehand, Live Paint, Path ops, Perspective
Grid, PNG ppi, Print, Save for Web, Shape Builder, Slices, Swatch
Options, Symbol options, Justification, Dimension, PDF page, the effect
dialogs' number rows). docs/ui-parity.md's number-field row says so.
Test: panels::stroke::tests::the_wheel_stops_weight_at_zero_and_limit_at_one
(the Stroke panel in a headless frame; focus Weight at 1 pt and turn the
wheel down four notches: the field shows 0 pt and the stroke is 0 pt;
the same with Limit at 2 x: 1 x and a limit of 1). It fails without the
change (the field showed -3 pt) and passes with it; the other numeric
field, scrub and dialog tests still pass.
The MCP drawing tools ran the creating command (path.create, shape.*)
and only then paint.setFill, paint.setStroke, stroke.set,
object.setProps and effect.apply, each as a command of its own.
check_draw_options checked dash, cap, join, opacity, name and effects up
front, but not the paints: a fill or stroke the engine can't read
(stroke: "currentColor"), or a swatch the document lacks, failed after
the object was made, so the call returned an error and left the path,
its undo step and its SVG node behind. Each styling command was also an
undo step of its own.
The tools now send the creating command and the styling commands as one
command.batch (crates/mcp/src/tools.rs, draw): the engine rolls the
whole batch back when a step fails, so a refused paint leaves no object,
no undo step and nothing in the SVG, and a good call is one undo step
named as the creating command names its own (Pen, Rectangle, …). The
styling steps act on the new object, which the creating command selects,
with item: null (its whole appearance), as the ids did before. The
reply gives the failed step's own error, without the batch's prefix.
docs/mcp.md says so.
Test: tests_drawopts::a_bad_paint_draws_nothing (invalid fill, stroke,
a missing swatch and a non-numeric strokeWidth on draw_path and
draw_shape: error, node count, undo history and SVG export unchanged;
a good call adds one undo step named Pen / Rectangle). It fails without
the change (draw_path with stroke "currentColor" drew a path) and passes
with it; the other draw option and command_batch tests still pass.
A line style's `Patn` was copied twice (filtered, then scaled) whatever its
length, before any budget saw it. Patterns longer than 64 lengths now import
as solid lines, with a warning.
Addresses CodeRabbit's third review on #3548.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Overlays bake child-first: a group's overlay now lies over its children's
(they used to be applied after it and win). Each one pays out of the
import's overlay work budget.
- Master page instances inside embedded documents are left out with a warning:
their indices count the embedded document's masters, which aren't kept, and
would show the outer document's.
- A text outline's gradient stops and dashes are charged to the text attribute
budget, like the fill's.
Addresses CodeRabbit's second review on #3548.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- One budget for the whole read (layers, pixels, characters, table
cells), shared with embedded documents and charged also when their
reading fails; the first reading is dropped before the second.
- Tables: at most 65 536 cells; lines and cell formats that don't
match the grid are left out instead of shifted.
- Paragraph formats matched to runs by binary search; font names
longer than 256 bytes are dropped.
- Overlays: one work budget per import; encoded originals decoded
within their layer's size; table cell outlines recoloured too.
- Engine: master page copies capped, each page's master found in
linear time, line gaps looked up once per font, the working CMYK
table built once, objects past MAX_COORD left out.
- Embedded content stays with its node; pixel masks under artistic
text are read whole and include stroke reach; gradient overlays on
hidden layers are kept.
- Render: contexts and raster effect images stay within
MAX_RASTER_SIDE per side (vello overflowed near u16::MAX; found by
the new fuzz target).
- Tests: affinity tests/import_limits.rs, feature samples in
import_fuzz.rs, shared builders in engine/tests/support/.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Strokes set to None no longer import as black 1px strokes.
- Underline and strikethrough are kept.
- Frame text starts at cap height; lines hanging out of a frame make it taller;
mixed-alignment artistic text lines up within its longest line; line breaks
inside a paragraph get no paragraph spacing.
- Table cell text taller than its cell is centred and stays visible.
- Outline effects on stroked shapes are drawn around the stroke.
- CMYK pixel layers go through the document's embedded CMYK profile; without
one, opaque layers stay CMYK.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
An export through the desktop save panel kept any typed extension
that names a known format, whatever format it wrote. Export As with
PNG chosen and card.svg typed wrote PNG data to card.svg, and Save as
PDF, Save for Office Documents, Text Export Options, Export Library
and the other writes that go through pick_path did the same.
document.exportSelection with a format suggested and added .png
whatever the format.
Such an extension is now replaced with the written format's, as Save
As has done since #3372: card.svg is written as card.png. An extension
of the written format stays as typed in any letter case (photo.JPEG
for JPEG), Export As SVG (and file.exportAs, file.export.svg) keeps
art.svgz and writes it compressed, and document.exportSelection
without a format still writes the format the typed extension names.
With a format, it suggests and adds that format's extension.
Docs: docs/target-app-parity.md.
File › Package… read every linked file and font and wrote the package
on the UI thread, so the window stopped responding until the package
was written. For a document that links 50 images of 21 MB each, OK
with Background Export off, which does the same work, takes a median
of 2.25 s in a release build on Windows.
With Enable Background Export on (Preferences › File Handling), OK now
takes the options, the report's Document Info text and a snapshot of
the document, and closes the dialog. The thread that writes background
saves and exports then collects the links and fonts and writes the
package. The same OK takes 0.001 s, and the package holds the same
bytes. A save of unsaved changes that Package starts is written first.
With the preference off, on the web and over MCP and scripts, the
package is made at once with the same reply as before.
Docs: docs/target-app-parity.md.
With the Artboard tool, Paste and Paste in Place select the pasted
artboards, Cut gives the tool the artboard before the first cut one
(the new first artboard when artboard 1 is cut), and tool.setOption
sets its selected or active artboard. The active artboard, the view's,
stayed where it was, so the navigator showed another artboard than the
tool and the Artboards panel. document.activate and File › Revert
started the tool on artboard 1, while choosing a document's tab starts
it on that document's active artboard.
After a command that changes the artboards the Artboard tool has
selected, the app now makes the tool's active artboard the active one,
as a click with the tool does. After a command that chooses another
document, and after Revert, the tool starts on the document's active
artboard.
Docs: docs/mcp.md, docs/target-app-parity.md.
The Layers list scrolls in its own scroll area, which kept egui's floating
scroll bar when #3093 gave the panel bodies a bar in a column of their own.
Hovering the list showed the bar over the right edge of the rows, over the
target circles and the selection squares the user clicks to select a layer.
The list now uses the menus' slim solid bar (list_scroll_style) in its own
column, and the rows end beside it. The list knows its total height before
it lays the rows out, so the bar is shown exactly when the rows overflow
(decided up front): the rows never shift sideways a frame after the bar
appears.
Test: dock::tests::the_layers_scroll_bar_covers_no_row (40 layers in a
1440x600 window: no row's accessibility rect intersects the bar). It fails
without the change and passes with it. The Layers tests that pin row
positions across frames still pass. Checked live: the bar sits beside the
rows.