- In `shaper::refresh`, replace full document walks with `changed_shaper_groups`,
diffing `before` and `after` layers and short-circuiting on pointer-equal (`Arc::ptr_eq`)
subtrees in O(1).
- Return whether Shaper groups rebuilt or modified artwork (`Result<bool>`),
allowing `Session::edit` to skip the second `cmd::objects_on_path::refresh`
call when no artwork was modified.
- In `textwrap::refresh`, check pointer equality on layers against `before.layers`,
skipping untouched layers in O(1) and checking only modified layers.
With a layer selected, Object > Hide > Unselected Artwork hid every object
on that layer, since the selected layer was dropped from the objects to
keep. The objects of a selected layer now count as selected.
While Lock Guides is on, a group that held a guide path was hidden whole,
which hid the guide with it. Guide paths now count as selected, so the
command keeps such a group visible and hides its other members one by one.
svg_darkness_uses_rendered_shadow_in_non_srgb_working_space runs itself
again in a child process with --exact and checked only the exit status.
libtest exits 0 when --exact matches no test, so a rename of the test
would have left the parent passing without running the non-sRGB check.
The parent now also requires "1 passed" in the child's output, as
layout_cache_follows_fonts_added_later in crates/tools does.
Main replaced the glob import of the command helpers in fileio/eps.rs
with an explicit list; the EPS Save path's selectedOnly check uses
bool_or, so it is added to that list.
File > Document Color Mode and document.open {colorMode} converted the
colors of the layers, symbols, pattern tiles and swatches only. Graph
designs, graphic styles, the art of opacity masks, the tile that
pattern editing's Cancel puts back and the art of brushes kept their
old color model (only a tint of a global swatch in a graphic style or
a mask followed the swatch), so a CMYK document could still apply,
place or print RGB colors from them.
These now convert with the rest, in the same undo step: their solid
colors, gradient stops, text runs and mesh points, nested masks
included. A Hue Shift brush's key color takes the converted color of
the art color it matches, a tint of a global swatch included. The
brush library is stored again only when one of its colors changed. A
tint of a global swatch in any of these places becomes the converted
swatch's color at its tint, as in the layers. The reply's changed
count covers the colors of the layers, symbols, graph designs, pattern
tiles and opacity masks; the colors of graphic styles and brushes
convert without being counted.
Docs: docs/mcp.md, docs/target-app-parity.md.
While an opacity mask or a pattern was edited, a Shaper scribble and
Object › Shaper › Release and Expand ended isolation, and a Shaper
double-click on a Shaper Group isolated the group's original shapes.
Each replaced the editing layer's isolation while the document stayed
in mask or pattern editing. A click on the Shaper Group's mode widget
did the same through object.isolate.
A scribble, Release and Expand now keep the editing layer isolated in
these modes, and end isolation as before outside them. A double-click
with Shaper selects the original shape under the pointer without
isolating the group's original shapes, and the mode widget does not
show while a mask or a pattern is edited.
Docs: docs/shaper.md, docs/target-app-parity.md.
Pattern Brush Options has a menu below the Outer Corner Tile and the Inner
Corner Tile. Each can be the brush's own corner art (Original), None, or one
of four tiles made from the side tile at each corner of the path:
- Auto-Centered stretches one side tile around the corner, centered on it.
- Auto-Between bends a side tile on each side into the corner, the two
meeting on the corner's diagonal.
- Auto-Sliced lays a side tile on each side past the corner and masks it
at the diagonal, so the two meet like a mitered frame.
- Auto-Overlap lays a side tile on each side past the corner, overlapping.
The tiles are straight at the corner and follow the path towards the side
tiles, so they meet a curve that runs into the corner. A side tile taller
than the corner can reach is stretched along until the two sides meet. On a
short path, a corner's tile is squeezed along so that it covers at most the
path up to an open path's end and half way to the next corner, and its two sides still meet on the
corner's diagonal.
The definition stores the choice as `outer_source` and `inner_source`
(`none`, `autoCentered`, `autoBetween`, `autoSliced`, `autoOverlap`), which
`brush.options` sets. Corner art wins over a source. A brush saved without
a source draws as before: a kind of corner without art takes the other
kind's art. None leaves that kind of corner empty.
Docs: docs/mcp.md, docs/target-app-parity.md.
Enter Isolation Mode worked while an opacity mask was edited, and Edit
Opacity Mask and View Opacity Mask worked in isolation mode, so each
mode replaced the other's isolated art. In the reference app, isolation
mode can't be entered while a mask is edited, and mask editing can't be
entered in isolation mode.
object.isolate and perspective.editText, which shows type in isolation
mode, are now off while an opacity mask is edited, and a call returns
an error that ends "stop editing the opacity mask first". In isolation
mode transparency.editOpacityMask and transparency.viewOpacityMask are
off, and transparency.makeOpacityMask refuses one target, an object or
a layer, which it would take into mask editing; a call returns an error
that ends "exit isolation mode first". Make Opacity Mask with two
objects still masks one with the other. Pattern editing mode does not
count as isolation mode for these commands.
Object > Perspective > Edit Text, the canvas context menu and the
Transparency panel's menu and Make Mask button follow the commands'
enablement. A click or Alt-click on the mask thumbnail in isolation
mode shows the error and does not edit the mask. While a mask is
edited, the Contextual Task Bar leaves out Isolate, and the Properties
panel's Quick Actions leave out Isolate Group.
Docs: docs/mcp.md, docs/target-app-parity.md.
The reference app disables its Lock, Hide and Show All menus in
isolation mode. In VectorCraft every item of Object › Lock and
Object › Hide, Unlock All and Show All worked there.
In isolation mode, object.lock, object.lock.above,
object.lock.otherLayers, object.hide, object.hide.above,
object.hide.otherLayers, object.unlockAll and object.showAll are now
off. Their menu items are greyed out, and a call returns an error
that ends "exit isolation mode first". The Contextual Task Bar's
Lock button is greyed out too, and the Properties panel's Quick
Actions leave out Lock. Pattern editing mode and opacity mask editing do not count as
isolation mode for these commands. The check is outside_isolation in
cmd.rs, with has_selection_outside_isolation beside it.
Docs: docs/mcp.md, docs/target-app-parity.md.
A Blob Brush stroke used to merge with every closed path of its fill
color and no stroke that it touched, anywhere in the document, across
layers and groups and past objects of other kinds. It now merges with
the topmost such path it touches, and with the other touched matching
paths in the same group or layer that are next to that path in
stacking order, with no other kind of object between them. Merge only
with selection still limits the candidates to selected paths. The
merged path keeps the topmost path's id and place.
The reference app places a new layer below the selected layer with
Ctrl+Alt-click (Command+Option-click) on the New Layer button, and a
new sublayer at the bottom of the layer list with the same click on
the New Sublayer button. In VectorCraft that click opened Layer
Options, and New Layer and New Sublayer always added the layer above
the current layer and the sublayer on top of the layer's contents.
layer.new takes below, which adds the layer directly below the current
layer at its level, and layer.newSublayer takes bottom, which adds the
sublayer under the parent's contents. In a clipping layer the new
sublayer goes above the clipping path. Each is one undo step, and
below is refused together with top. A Cmd/Ctrl+Alt-click on the
Layers panel's New Layer and New Sublayer buttons runs them. A plain
click, an Alt-click and a Cmd/Ctrl-click work as before.
Docs: docs/mcp.md, docs/target-app-parity.md.
A .ai file whose editing data has no layers, as swatch libraries are
saved, opens from its PDF part, and the PDF part has the default
swatches. layered_ai applied the editing data's palette only when it
built the document from the layers, so Other Library… listed the
default swatches for such a library, and File › Open gave the file the
default swatches as well. A .ai whose layers can't be used (an image
too large for the editing data's reader, or a read error) lost its
palette the same way, and so did an EPS that opens as its page.
The reader now reads the palette on its own (ai::palette, which shares
palette_swatches with the layer reader), and its swatches and color
groups replace the default swatches in those three cases. Spot and
pattern swatches of the PDF part's art that the palette does not have
follow the palette's swatches.
Fixes#4100. Part of #3349.
The Type tool heard of Undo and Redo only when they ran as top-level
commands. As steps of command.batch, they left the caret where it was,
and the tool took the batch for an outside edit. Session::execute now
tells the active tool about each nested edit.undo and edit.redo.
command.batch also tells the tool when it begins, ends or rolls back.
The Type tool keeps its caret and its edit records at the start of each
batch and puts them back when a failed batch rolls the documents back.
GIF, WebP and Targa files hold no resolution field, so document.formats and
the export docs now say that ppi, and the resolution that width or height
gives, are stored in the file except in those three formats.
PNG, PNG-8, JPEG, WebP, GIF, TIFF, BMP, Targa and PSD exports take
`width` or `height` in pixels. Each file is scaled so that its own
artboard, `rect` or art bounds is that wide or high, so with
`useArtboards` every file gets its own scale. `width` wins over
`height`, and either wins over `ppi` and `scale`, in the order an
Export for Screens row reads its size. The scale stays within 0.01 to
64 pixels per point and the file stores the resolution that results.
A value that is not a positive number is refused.
The per-file scale comes from `ScreenSize::scale_for`, which Export for
Screens rows already use. Rows keep their own size: their format
settings still drop `width` and `height`. `document.formats` lists the
two options for every raster format.
The check after the last file returned the cancelled error for every
export. A folder export without a ZIP has written each file by then, so
a cancel that came during a final step that writes nothing (a row whose
file repeats the one before it) reported a finished export as
cancelled. The check now runs only for exports that still hold their
files: those without a folder and those that make a ZIP.
A cancel that came after the last file's check was not seen: an export
without a folder returned its files or its archive anyway. The export now
checks once more after the last file and again after deflating the
archive, so a cancel returns the cancelled error and nothing is written
or returned.
zip::deflate takes a `stop` callback that it calls before each 1 MiB of
a file. Export for Screens passes the cancel state, so deflating stops
at the next MiB. file.package passes one that never stops, so its
behavior does not change.
With `zip: true`, document.exportForScreens deflated nothing: every file
went into the archive stored. Each file is now deflated, or stored when
deflating does not make it smaller, as file.package's archive does. The
file list, the names and the reply's fields stay the same; `bytes` and
the archive's contents change. Entries keep the fixed 1980-01-01 date, so
the same files still give the same archive.
The deflating writer moves from package.rs into fileio/zip.rs as
`zip::deflate`, beside `zip::store`, and file.package calls it there with
its own date and error text. The test reader `zip::entries` reads
deflated entries too. The `zip` option's text in docs/mcp.md,
docs/control-protocol.md and the command's params says the files are
deflated.
An Alt-click right of a short layer name, inside the wider rename
target, no longer replaces the selection. The isolated layer's own row
now selects the art inside it (layer.selectAll), where it cleared the
selection before. The doc line says the click selects visible, unlocked
art and clears the selection on a hidden or locked layer.
The reference app switches the units of measurement with
Ctrl+Alt+Shift+U on Windows and Option+Shift+Command+U on macOS.
VectorCraft could set the document's units with document.setUnits or
a right-click on a ruler, but no command stepped through them and no
key did it.
document.switchUnits (Switch Units, default Cmd+Alt+Shift+U) sets the
document's units to the next one in the order a right-click on a
ruler lists them: Points, Picas, Inches, Millimeters, Centimeters,
Pixels, Feet & Inches, Meters, Yards, Feet, and from Feet back to
Points. Each press is one undo step, and the reply names the new
units. Keyboard Shortcuts lists it under Document.
The precise cursors shortcut-editor test used Cmd+Alt+Shift+U as a
free key; it now uses Cmd+Alt+Shift+Q.
Docs: docs/mcp.md, docs/ui-parity.md, docs/target-app-parity.md.
Deleting a point of a finished path with the Curvature tool sent
path.removeAnchor, which refits the two segments that met at the point so
one curve follows them. The path kept the bend of the deleted point
instead of curving through the points that remain.
The tool now sends path.removeAnchor with curvature: true, which uses the
new curvature_remove. Each smooth neighbor takes new handles from its new
neighbors, and an end beside one of them takes its end handle again. A
corner neighbor keeps its handle directions, and its handle on the new
segment scales with the segment's chord. With two points left, the points
that curve lose their handles. Object > Path > Remove Anchor Points and
the Delete Anchor Point tool still refit the curve.
An anchor added to a path that wasn't selected now selects that path,
the new anchor direct-selected, so the point shows and can be edited at
once instead of only after a Direct Selection click. Undo restores the
old selection; a path already selected (the Pen's Auto Add) keeps it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
edit.colors.saturate's params text names intensity as required, but a
call without it, or with a string such as "40", read the intensity as
0 and still ran Saturate. It changed no visible color but recorded a
Saturate undo step.
edit.colors.saturate now reads intensity with f64_req, as shape.ellipse
reads its box. A missing, null or non-number intensity is the error
"missing number `intensity`", which changes nothing and records no undo
step. A number outside -100..100 still counts as its nearest end, and
the params text says so.
Saturate returns a color whose saturation stays the same, as at an
intensity of 0, as it was. The HSB round trip rewrote most such colors
in their last bit, so they counted as changed and lost their swatch
link.
In the same file, edit.colors.adjustBalance read a channel that is not
a number as 0, and color.harmony read steps or amount that is not a
number as its default. Both now refuse it with check_numbers, as the
commands docs/mcp.md lists for #1952 do. A null is still read as a key
left out.
Docs: docs/mcp.md, docs/target-app-parity.md.
layer.new, layer.selectAll, layer.target, layer.move, layer.template,
layer.pasteRemembersLayers and layer.releaseToLayers read their on/off
params with bool_or or as_bool, which falls back to a default for a
value that is not true or false. layer.new {"top": "true"} added the
layer above the current one instead of on top, layer.template
{"on": "false"} and layer.pasteRemembersLayers {"on": "false"} turned
the option on when it was off, layer.move {"copy": "true"} moved the
rows instead of copying them, layer.selectAll and layer.target with
"add": "true" replaced the selection, and layer.releaseToLayers
{"build": "true"} released the objects without building up copies.
These commands now fail with "`<key>` must be true or false" for a
value that is not true, false or null, as the artboard and Object menu
commands do, and leave the document, the selection, the highlighted
rows and the undo history as they were. layer.newSublayer, which takes
layer.new's options, refuses such a top too. A null is read as a key
left out.
docs/mcp.md also lists showCenterMark, showCrossHairs and
showVideoSafeAreas among the keys artboard.new and artboard.setProps
refuse in this way, as they already did.
Docs: docs/mcp.md, docs/target-app-parity.md.
artboard.setProps refused an x, y, width or height given as text, but
artboard.new read such a value as the key left out: {"x": "10"} added
an artboard at the default position and recorded an undo step. Both
commands read a name that is not a string, such as 5, as no name, so
artboard.new gave the default name and artboard.setProps kept the old
one.
artboard.new now refuses an x, y, width or height that is not a number
with "`<key>` must be a number, not <value>", the message
artboard.setProps gives, and both commands refuse such a name with
"`name` must be a string", for each entry of artboard.setProps'
artboards too. They refuse it before they change anything. A null is
still read as the key left out.
Docs: docs/mcp.md, docs/target-app-parity.md.
Found while testing crates/engine/src/cmd/create.rs:584 (dividers)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>