The Artboard Options help pages describe two display options
(https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/organize-manage-artboards/modify-display-settings-of-artboards.html,
.../set-video-display-options-for-artboards.html): a mark at the artboard's
center, and lines from the middle of each side through the center. The
document already had show_center_mark and show_cross_hairs on Artboard, but
nothing set or drew them.
artboard.setProps (each `artboards` entry too) and artboard.new take
showCenterMark and showCrossHairs as true/false, in the same single undo
step as the other fields; a locked artboard can still change them.
Artboard Options has both checkboxes, filled in from the artboard whether
it opens from the panel or a double-click with the Artboard tool, and its
New Artboard form passes them to artboard.new. The canvas draws them in the
new theme token artboard_mark, 1 point wide on screen at any zoom: a cross
12 points across at the center whose arms follow the artboard's sides, and
the two cross hairs. They are screen-only like the edges; exports and print
never read the fields. inspect_document reports them when on. Saved files
are unchanged: the two keys were already always written, so a document that
never shows them saves byte for byte as before, and a file without the keys
opens with both off. Video safe areas, video rulers and the pixel aspect
ratio are not part of this change.
Verification: artboard_display_marks_are_set_and_cleared_in_one_undo_step,
artboard_display_marks_round_trip_through_the_native_format,
artboard_display_marks_stay_out_of_exports (SVG and PDF identical with the
marks on and off), an_artboard_shows_its_center_mark_and_cross_hairs
(positions, screen-constant size under zoom, rotated view) and
artboard_options_show_center_mark_and_cross_hairs. cargo xtask ci: all 10
steps passed.
The Simplify help page says the slider starts at an auto-simplified value
(https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/modify-paths/manually-simplify-paths.html).
The slider opened at a fixed level 50 (1 pt), whatever the size of the
selection, so the first preview was the same tolerance for a 30 pt icon as
for a 1000 pt drawing.
The level Auto-Simplify picks for the selection is now one engine function,
vectorcraft_engine::auto_simplify_level (the existing 1/400-of-the-longer-
side rule in vectorcraft_pathops), which object.path.simplify {auto: true}
and the slider's opening fields both use. Object › Path › Simplify (and
ui.menuDialog, the Properties and contextual task bar buttons, which go
through the same menu dialog) opens the slider there, and the canvas
previews that result; Auto-Simplify still moves the slider back to it.
The retained Simplify dialog path is unchanged.
Verification: simplify_slider_opens_at_the_auto_simplify_level_of_the_selection
opens the slider on 1140, 285 and 28.5 pt waves (levels 35, 55, 89) and
checks the preview is exactly object.path.simplify {auto: true}'s result;
without the change it fails with level 50 != 35, and
simplify_opens_a_slider_whose_drag_previews_fewer_or_more_points fails with
{"level": 50} != {"level": 55}. cargo xtask ci: all 10 steps passed.
Export for Screens and the Asset Export panel's Export encoded and wrote
every file on the UI thread, so the window stopped responding until the
last file was written. For 50 artboards in PNG at 1x, 2x and 3x, PDF and
SVG (250 files, 47 MiB), the dialog's OK took a median of 43.8 s in a
release build on Windows.
With Enable Background Export on (Preferences › File Handling), OK now
checks the settings, remembers them and takes a snapshot of the
document, and the dialog closes. The thread that writes background
saves and exports then encodes the files and writes them into the
folder, or one ZIP in it, and the export progress dialog's Cancel stops
it before the next file. The same OK takes under 0.001 s, and the files
have the same names and bytes. With the preference off, without a
folder, on the web and over MCP and scripts (document.exportForScreens
and assets.export), the files are made at once with the same reply as
before.
Both commands report one progress step per file, so the progress dialog
shows "File 3 of 250" and MCP progress notifications count the files in
place of the encoders' artboards and pages.
Docs: docs/target-app-parity.md, docs/control-protocol.md, docs/mcp.md.
A filter plug-in gets 50 M instructions and a minute for each run.
Filters that order a plotter's paths, pack shapes or run a simulation
need minutes, and the app shouldn't freeze while they compute.
A filter's manifest may now ask for "budget": "heavy" (Budget): each run
gets HEAVY_FUEL (1 G instructions) and HEAVY_WALL_TIME_MS (300 s). Only a
filter may ask. plugin.install refuses such a filter without allowHeavy:
true, and plugin.inspect returns the entry a module would have without
installing it. Install Plug-in… and File › Open ask the user and install
on OK; the plug-in store keeps the answer, and folder loads skip heavy
filters with a note, since they ask nobody.
The app runs a heavy filter on a thread of its own (plugin_runs), not in
Background Save's queue, with the status bar and a progress window
(Cancel, Continue in Background) that it shares with Background Export
(export_progress::window). FilterRun splits plugin.run: prepared on the
session, run on any thread, its output handed in, and the next
plugin.run with the same params applies that output instead of running
the plug-in again, so the edit stays one guarded, journaled command. A
result whose document or plug-in changed meanwhile is dropped, and the
status bar says to run it again. A cancelled plug-in run is now
EngineError::Cancelled.
What a filter keeps with the document (#3720) goes with the run: it is
given _context.documentData when the run is prepared, and the
documentData of its output is applied with its objects, in the same undo
step, when the run is handed in (FilterRun::finish).
Tests: plugins/tests/abi.rs (100 M loop steps out of a light budget,
within a heavy one; folders skip heavy filters), manifest.rs, engine
tests_plugins.rs (consent, inspect, the store, hand-in, a handed-in
run's document data, cancel) and
ui-egui tests_plugin_runs.rs (installing asks, no Preview, a background
run as one undo step, a stale result kept out, the progress window).
CRAFT_UI_HEAVY_FILTER_WASM=<folder> writes a test filter to try in the
app. Its strings and messages have rows in each complete catalog
(COMPLETE_MESSAGES).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhvnnnD5fTFupK8WWq35un
Hyphenation... and Justification... already shipped upstream (#2310,
#2755). This ports the part of the archived fork PR #10 that upstream
lacked: a paragraph's Justification > Auto Leading now also sets
- the Leading the Character (and Properties) panel shows for Auto type,
blank where the selected paragraphs differ;
- the leading Alt+Up/Down (type.step) steps from, per paragraph (a run
spanning paragraphs is split so each takes its own);
- the lines Fill with Placeholder Text counts in area type.
ParaStyle::effective_leading mirrors the layout (exactly
CharStyle::effective_leading at the default 120%). Tests in doc, engine
and ui-egui; parity docs no longer list Hyphenation/Justification as
missing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014gCpjL3yUhaH3jRp7ksufh
A Shape Builder drag merged only the regions the pointer passed over,
and the tool did not read Shift. The reference app's Shape Builder page
describes a Shift-drag that draws a rectangular marquee to merge many
paths at once.
With Shift held at the press, the drag now draws a rectangular marquee
from where it started to the pointer. The regions it reaches are shaded
while it is drawn and merge into one shape on release; with Alt also
held at the press, they are deleted. The marquee reaches each region
whose outline crosses it or has an anchor inside it, and the region that
holds the whole marquee. A Shift-click that does not move is a click,
and a drag without Shift works as before. With In Merge Mode, Clicking
Stroke Splits the Path on, a Shift-click on a stroke splits it as a
click does; a marquee splits no stroke, and while it is drawn the
pointer does not show scissors and no stroke is highlighted.
shapeBuilder.merge takes the marquee as rect: [x, y, width, height],
where x and y are where the drag started, and points may then be left
out. For Pick Color From Artwork, the corner where the drag started
counts as the drag's first point, and the opposite corner counts as its
last. With rect, splitStroke does not split a stroke.
Docs: docs/mcp.md, docs/target-app-parity.md.
Cause: with Snap to Point, the point of a moved selection that snaps to a
ruler guide is the grabbed point (#1534). Away from an anchor or center
that was the raw pressed spot (select.rs: `grab` fell back to `p`;
direct.rs: a straight segment's `grab: p`, MoveObject's `start`). A press
on a stroke or within the pick tolerance of an edge lies a few pixels
beside the path, so the pressed spot landed on the guide, "align" showed,
and the path stopped that far beside it: 0.096 pt at 3,891% and 0.0099 pt
at 28,757% in the #3701 screenshots, visible only when zoomed in.
Change: `guides::hold_on_path` finds the nearest point of the path (on a
straight segment exactly on its line). `select::hold_point` uses it for a
press on the stroke or outline, or within 6 px of it. The Selection tool
(`State::Moving::hold`), Direct and Group Selection moves
(`State::MoveObject::hold`) and Direct Selection's straight-segment drag
(`grab`) snap that point. The move delta still runs from the press, so
nothing jumps. A press in the middle of a fill still snaps the pressed
spot, as Snap to Point is documented to.
Verification: tests_prefs::a_move_held_by_a_paths_stroke_lands_the_path_on_a_ruler_guide
and tests_prefs::a_straight_segment_dragged_by_direct_selection_lands_on_a_ruler_guide
fail without the change (edge 0.0257 pt and anchors -0.0052 pt off the
guide) and pass with it (|gap| < 1e-9 pt). A headless egui/wgpu render at
3,891% and 28,757% puts the guide, the selection outline and the centre of
the 1 pt stroke band in the same pixel column. cargo xtask ci: all 10
steps passed; 9429 tests passed, 0 failed.
Review follow-up (CodeRabbit): a text node's characters carry their own fill
(CharStyle), saved apart from the node's appearance, so conform also checks
the runs of the label. dimension_colours_follow_the_document_colour_model
opens a CMYK document whose label characters alone are RGB; without the
change they stay RGB.
A filter plug-in had nowhere to keep settings that belong to the whole
document rather than to one object, such as the pens a plotter holds or
a machine profile: it could only ask for them again on every run.
- A filter's output may hold documentData: any JSON of at most 1 MiB
(objects::MAX_DOCUMENT_DATA) that replaces what the plug-in keeps with
the document, null removing it, in the same undo step as its objects
(objects::decode_output). Too much refuses the output and the
document stays as it was.
- A filter gets it back in _context.documentData (null until it keeps
something). A filter reads and writes only its own.
- It is the document's foreign data, Document::unknown["pluginData"] by
plug-in id (vectorcraft_doc::plugin_data), where the perspective grid
and the brushes keep theirs. So the native file needs no section of
its own for it, and hosts from before keep it when they save. An
exporter is given the whole document, so it can read every
plug-in's. Nothing else lists it, as the reference app's Document
Info lists no plug-in data.
A live effect isn't given it: its geometry is worked out from the
object alone wherever it is drawn.
Tests: plugin_data.rs (each plug-in its own entry, forgetting what isn't
kept copies nothing, a damaged value holds nothing), objects/tests.rs
(kept, forgotten, refused over the limit), tests_plugins.rs (one undo
step with the objects, read back, not given to another filter, kept by
the native file among its foreign data, refused over the limit with the
document unchanged, null removes it) and tests/plugin_export.rs (an
exporter finds another plug-in's). The size error has its row in each complete
catalog (COMPLETE_MESSAGES).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhvnnnD5fTFupK8WWq35un
Follow-up to #3702 (CodeRabbit): dimensions are only redrawn when their
recipe changes. Document Color Mode converted their art through the colour
profile, so black became a four-ink C67 M67 Y67 K100 again, and CMYK files
saved before #3702 kept RGB dimension art.
dimension::refresh now also redraws when the document's colour mode
changes. dimension::conform, run when a document is opened or reverted
(Session::add_document, replace_document), redraws a dimension whose art
has colours in the wrong model; one that can't be redrawn keeps its art.
dimension_colours_follow_the_document_colour_model covers RGB to CMYK and
back, and a CMYK document with RGB dimension art being opened. Without the
change it finds C0.67 M0.67 Y0.67 K1 after Document Color Mode.
With In Merge Mode, Clicking Stroke Splits the Path on, the run a click
split off could run against its path. A click on a rectangle's top side
gave a side from (100, 0) to (0, 0) on a rectangle that runs from
(0, 0) to (100, 0) there, so arrowheads and other settings that follow
a stroke's direction moved to the other end. The run now goes the way
the path goes.
The two paths of a split, and what an Alt-click or a merge leaves of a
path, had a gradient fitted to the path's bounds fitted again to their
own bounds. They now keep it where it was on the path.
A run that left 0.05 pt or less of a path still split it, into the
run and a speck. Such a path now does not split, and the tool shows
no scissors over it.
Docs: docs/mcp.md (Shape Builder Tool Options).
Review follow-up (CodeRabbit): a linear gradient expands to a mesh with a
column at each stop, and the mesh mixes linearly between columns, so a
Perceptual blue -> yellow came out as the grey RGB mix in the middle.
`breaks` now also takes a column at each stop the Perceptual expansion adds
(Gradient::expanded), so the mesh follows the OKLab curve; Classic is
unchanged. the_mesh_option_makes_a_mesh_that_paints_the_gradient gains a
Perceptual case; without the change 1812 pixels differ from the gradient.
Illustrator 30.0's Method choice in the Gradient panel
(https://helpx.adobe.com/illustrator/desktop/paint-and-fill/create-and-edit-gradients/control-perceptual-interpolation-in-gradients.html).
- paint.editGradient takes `method: classic|perceptual` (one undo step);
paint.setFill/setStroke take it in `gradient`, and gradient params (swatches,
copied paints) carry it when Perceptual. Anything else is refused without
editing. One edit can set `method` and `dither` together.
- The Gradient panel shows a Method menu (Classic, Perceptual) for linear and
radial gradients, dimmed without a gradient; the Dither box stays under the
stops.
- i18n: "Perceptual" where a complete catalog lacked it, and the `method`
error in all 24 catalogs.
- Docs: G13 (estimate 14–24 h → 13–23 h, after Dither's 15–25 h → 14–24 h),
target-app-parity (row and evidence), ui-parity, mcp.md, plugins.md.
Illustrator 30.0's gradient Method, from the public documentation
(https://helpx.adobe.com/illustrator/desktop/paint-and-fill/create-and-edit-gradients/control-perceptual-interpolation-in-gradients.html):
Classic or Perceptual; perceptual interpolation matches colour transitions to
how the eye perceives differences between colours, and perceptual gradients
are rasterised when saved as PDF or SVG.
- Gradient gains `method` (Classic | Perceptual; serde default Classic,
written only when Perceptual, so older files open as Classic and Classic
files are written as before). The colour crate's OKLab helpers open up for it.
- Perceptual mixes neighbouring stop colours in OKLab (sample, and
sample_keep_model for the stroke meshes, Expand and blends); opacity stays
linear. Renderers that mix linearly get stops on the perceptual curve,
placed by halving each half segment where a straight line strays more than
0.4 of a colour level (a nearly straight segment needs none). Classic
gradients expand exactly as before.
- draws_as_pixels includes Perceptual gradients, so SVG and PDF write a
Perceptual fill or stroke as an image at the document's raster effects
resolution, clipped to what it paints. A gradient along or across a stroke
stays slices (PDF no longer draws it as a within-stroke image).
- With Dither: a dithered Perceptual fill is the perceptual mix with dither's
noise (the canvas's dithered grids are keyed by method too); SVG and PDF
write it as an image with the noise, and a dithered Perceptual stroke as an
image without it, as the canvas draws it (GradientPaint::pixels, SVG's
raster_paint and PDF's area are told whether they paint a fill).
Review follow-up (CodeRabbit): the refusal skipped any request carrying a
`swatch` key, so `"swatch": null` with a dithered gradient got through.
Only a named swatch (a gradient swatch keeps its own dither) is exempt now.
The dither test sends `"swatch": null`; it fails without the change.
With Copy Links on and Copy Fonts off, File › Package still read every
placed PDF or .ai page to find the fonts of its type, and then dropped
them. With Copy Links on and Relink off, it still read the preview and
the PDF of each placed .vectorcraft document, which only the rewritten
copy uses.
Package now reads the placed pages for their fonts only with Copy Fonts
on, and it reads the preview and the PDF only with Relink on. With Copy
Fonts off, packaging a document that places the 60 pages of a PDF took
about 390 ms in a release build and now takes about 20 ms. The package
it writes is the same.
With the Artboard tool, File › Revert kept the active artboard, but a
command.batch with a file.revert step made artboard 1 the active one.
A batch that switched to another document and back, and the Actions
panel menu's Batch… over a folder whose files it closes or over data
sets with a Revert step, did the same. Each of them starts the tool
afresh and ends in the document it started in, and the app moved the
view to the fresh tool's artboard 1. Only a plain file.revert was
handled, by its command id.
The engine now counts the times switching or closing documents, or
replacing the active one, starts the tool afresh (Session::tool_resets),
and records whether the tool's options were set after that
(Session::tool_set_since_reset). When the count changes during a
command and nothing set the tool's options after it, the app starts
the tool on the active artboard, as it does when a command chooses
another document. When a later step chose the tool's artboards (a
Paste or Cut after the revert), the artboard the tool then has active
becomes the active one.
Docs: docs/mcp.md, docs/target-app-parity.md.
Review follow-up (CodeRabbit): paint.setStroke kept `dither: true` from its
gradient, and a stroke can hold dither anyway (swapped from a fill). Outline
Stroke and appearance expansion copied the stroke's paint into the outline's
fill, which then drew dithered although the stroke never did. The outline's
fill now goes without dither (Paint::stroke_as_fill), and paint.setStroke
refuses a gradient with `dither: true`, as paint.editGradient does.
With Pick Color From Artwork, a Shape Builder merge took the look of
the front-most object over the first covered region the drag touched,
and the current fill when the drag touched only areas no object
covers. The reference app's combine page gives the look of the object
where the drag starts, else of the object where it ends, else of the
topmost selected object.
shapeBuilder.merge now follows that rule: the merged shape takes the
appearance, opacity and blending mode of the front-most of the objects
it is given (the selection by default) under the first point, else of
the one under the last point, else of the front-most of those objects.
A drag from a hole across a gray bar into a red square now gives a red
shape, where it gave the bar's look. A click in an area no selected
object covers, such as a hole that gray bars enclose or a triangle
that lines enclose, takes the front-most selected object's look
instead of the current fill. Pick Color From Color Swatches works as
before. The tool already sends the press point first and the release
point last.
Docs: docs/mcp.md, docs/target-app-parity.md.
Illustrator 30.0's gradient Dither, from the public documentation
(https://helpx.adobe.com/illustrator/desktop/paint-and-fill/create-and-edit-gradients/control-dither-in-gradients.html):
a Dither checkbox in the Gradient panel; dither converts the gradient into
pixels with subtle noise, works with gradient fills only, and follows the
Document Raster Effects resolution.
- Gradient gains `dither` (serde default false, written only when on, so
older files open unchanged and new files keep it).
- The renderer draws a dithered linear or radial fill as an image paint: the
gradient sampled a cell per device pixel or finer (quarter-octave steps,
cached per thread), each channel given seeded triangular noise of up to a
level before rounding. Same pixels every run; strokes and undithered fills
draw exactly as before. PNG and other raster exports go through it.
- SVG and PDF write a dithered fill as an image at the document's raster
effects resolution, clipped to the shape (the freeform-gradient path);
characters' fills stay vector gradients in SVG, with a warning.
- paint.editGradient and paint.setFill take `dither`; strokes refuse it.
The Gradient panel's Dither box is dimmed behind the Stroke proxy.
- i18n rows in all 24 catalogs; docs: G13, target-app-parity, ui-parity,
mcp.md, plugins.md.
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 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.
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.
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>