The reference app resets tracking and kerning to 0 with Cmd/Ctrl+Alt+Q.
VectorCraft had no command for it.
type.resetTracking (Reset Tracking and Kerning, default Cmd+Alt+Q) sets
tracking to 0 and kerning to Auto, which the Character panel shows as 0,
in the text the Type tool has selected, or else in all the text of
every selected type object. At a caret it resets only the kerning of
the character before it, where Alt+Left/Right kerns. It is one undo
step. The range logic of type.step moves into a shared type_ranges.
When a call gives ids, type.step and type.resetTracking change all the
text of those type objects even while the Type tool edits text, as
paint.setFill does. Before, type.step changed the Type tool's selected
text instead.
Docs: docs/mcp.md, docs/target-app-parity.md.
A Ctrl/Cmd+Alt-click on a row's eye in the Layers panel ran Hide Others
or Show All Layers, as an Alt-click does. It now runs Outline Others for
the row's layer (layer.outlineOthers), or Preview All Layers
(layer.previewAll) when every other top-level layer is in Outline
already. Each is one undo step. Alt-click and Ctrl/Cmd-click on an eye
keep their actions.
Docs: docs/mcp.md, docs/target-app-parity.md.
Type is drawn as its glyph outlines under a geometry effect, in EMF and
WMF export, in DXF export when it is written as outlines, through
Flatten Transparency with Convert All Text to Outlines, and inside an
envelope. Those outlines differed from the type the canvas draws in
four ways. They had no underline or strikethrough bars. A character
stroke on scaled type kept its unscaled weight. An unplaced gradient was
fitted to the glyphs it painted instead of the layout box. A run's
pattern tiles stayed in document space instead of scaling and turning
with the type. A group's or layer's own fills and strokes also painted
the glyphs of the type in it without the bars.
The outlines now include each run's underline and strikethrough bars,
as the canvas has drawn them since #850. Each run's path is made in
text space with its unplaced gradients fixed to the layout box, then
placed by the type's transform, which also scales its stroke weight
and dashes and transforms its pattern tiles. The type's own fills and
strokes have their unplaced gradients fixed to the bounding box of the
layout box in the document, where the canvas fits them.
Geometry effects that move or scale the outlines move and scale the
type's gradients with them: the type's own effects, also inside an
envelope and after Flatten Transparency has converted the type to
outlines, and the effects of a group, symbol instance or envelope that
holds the type. outline_text marks its group with a new node field,
type_outlines, which is saved with the document. reshape_leaves maps
the placed gradients in each marked group through the map that takes
the group's bounds before the effects onto its bounds after them. A
scale out of proportion stretches a radial gradient into an ellipse
and turns the isolines of a diagonal linear gradient, as it does on
type scaled by its own transform. Placed gradients outside those
groups stay where they are.
Export Selection, Asset Export, the Assets tab of Export for Screens
and the rasterized art of CSS Properties cropped each object to its
visual bounds, which leave out live effects. A path with an 8 pt drop
shadow was exported with the shadow cut off at the path's edge, and
a group's own shadow was cut off the same way.
The crop is now the bounds the renderer measures for what it draws
(`vectorcraft_render::effect_bounds`): `painted_bounds` without its
room for brush art. A brushed path is cropped to its visual bounds as
before, and a Shaper group leaves out its transparent source art as
its visual bounds do. `assets.list` reports the same crop. The
renderer measures a shadow's or glow's reach on every side of the
object, so the image also has transparent room on the side away from
an offset shadow.
Docs: docs/mcp.md, docs/target-app-parity.md.
PDF, EPS, EMF/WMF and raster exports, and SVG exports that outline or
embed fonts, warn when type is set in a font family that isn't
available, but file.print and print.preview gave no such warning. A job
in a missing family printed in Source Sans 3 with no warning, and the
Print dialog's Summary showed none.
Both commands now add the exports' warning for the type that prints:
visible type that fits its frame or path, on the layers that Print
Layers picks, where what it paints (its effects applied) lands on the
paper of an artboard or tile that prints, inside its clipping paths.
The type in the symbols, inline graphics, patterns, scatter brushes,
opacity masks and placed documents of the art that prints counts too.
file.print reads the fonts before raster effects and Print as Bitmap
turn type into images, for PDF and PostScript output.
Docs: docs/mcp.md (Print), docs/target-app-parity.md (Fonts).
A data set can hold values for only some variables: `dataset.new` and
`dataset.set` store only the values they are given, and selecting the
data set leaves the other variables alone. Update Data Set replaced the
active data set's values with the values the art reports for every
variable. After one Update, a data set that set only a visibility
variable also held the current text of the bound type, and selecting it
again wrote that text back. Update also dropped a variable's value when
its objects were locked or inside a hidden or locked layer or group,
because such objects report no value.
`dataset.update` now writes the art's current value for each variable
the active data set holds, and keeps the data set's value when the art
reports none. A variable that the data set does not hold stays out of
it, including one defined after the data set was captured;
`dataset.update {all: true}` also adds the other variables that the art
reports a value for. The reply adds `left`, the variables it did not
write. The Variables panel's Update Data Set button and menu item run
it without `all`.
Docs: docs/mcp.md, docs/target-app-parity.md.
The Pen's pointer showed the new-path asterisk before the first anchor
and also while a path was being drawn, where a click adds the path's
next anchor. A new Cursor::PenDrawing, the pen nib without a badge, now
shows there. The close, convert and join badges still show over the
first anchor, the last anchor and another open path's end. The asterisk
shows only where a click starts a new path.
The Continue, Add and Delete badges now also show after another path
was selected while the Pen was still drawing (for example with a
Cmd-click). A click there already continued the path or added or
deleted an anchor, but the pointer showed the asterisk.
View › Proof Setup had two color blindness items, protanopia and
deuteranopia. A third item, Color blindness – Tritanopia-type, proofs
the canvas with a tritanopia simulation, and the Separations Preview
panel's Proof Setup list offers it too. `view.proofSetup` takes
`target: "tritanopia"`.
The simulation follows Brettel, Viénot and Mollon (1997): a color
keeps its L and M cone responses, and its S response is replaced so
that it lies on one of two half-planes. Both contain the neutral
axis; one passes through the 485 nm color and the other through the
660 nm color. The neutral axis runs to the display's white, so grays
keep their value. The matrices are computed at
compile time from the published inputs cited in the code. The
protanopia and deuteranopia matrices are unchanged.
In the Transform panel, the Properties panel's Transform section and
the Control bar, Alt+Enter (Option+Return) in X, Y, W, H, Rotate or
Shear ended editing as Enter does, and the selection itself moved,
resized, turned or sheared. Now the field's command gets `copy: true`
when the Enter that committed it was pressed with Alt. The copy takes
the value, the original stays where it was, and the copy is selected,
in one undo step. A press elsewhere with Alt held applies the value
without a copy. With anchor points selected with Direct Selection,
Alt+Enter applies the value to them, as Enter does.
- widgets::enter_modifiers returns the modifiers of the frame's Enter
press.
- transform::alt_enter_copies gives the `copy` param of the three
surfaces' commands.
Docs: docs/target-app-parity.md.
A Pencil or Paintbrush stroke always left the path it drew or continued
selected. The reference app's Pencil and Paintbrush options have Keep
selected for this, and VectorCraft's Tool Options had no such row.
The Pencil and Paintbrush Tool Options now have a Keep selected
checkbox, on by default, which keeps today's behavior. When it is off,
a new path is not selected and the selection stays as it was, and a
continued path is removed from the selection while the rest stays. The
stroke is still one undo step. The option persists like the tools'
other options, and path.freehand and brush.freehand take keepSelected.
Docs: docs/target-app-parity.md, docs/ui-parity.md, docs/control-protocol.md.
Ctrl/Cmd+Shift-clicking New Swatch in the Swatches panel opened New
Swatch for a spot color, as a Ctrl/Cmd-click does. It now opens the
dialog for a global process color, and with Alt also held it saves one
at once. `ui.newSwatch` takes `global`.
An Alt+Ctrl/Cmd-click with a gradient or pattern as the active paint
failed with "spot, mode and group apply to solid colours only". It now
saves a gradient or pattern swatch, as a Ctrl/Cmd-click does through
the dialog.
The reference app's Drop Shadow has a Color and a Darkness, the
percentage of black added to the object's own colors. Drop Shadow had
only a Color, so every shadow was one color. Choosing one or the other,
and the defaults, are VectorCraft's own.
Drop Shadow now takes `shade` ("color" or "darkness", default "color")
and `darkness` (%, default 100). Its dialog and the Appearance panel's
inline editor show Color and Darkness as radio buttons. With Darkness
d, a CMYK document keeps the object's cyan, magenta and yellow and
raises its black from K to K + d × (1 − K), and an RGB document
multiplies each channel by 1 − d, so in RGB 0% gives the object's
color and 100% gives black. A shadow saved without these keys draws as
before. SVG export writes a Darkness shadow with feComponentTransfer
and SVG import reads it back; CSS export leaves it out.
Docs: docs/target-app-parity.md.
Shift+Tab ran Tab's command and hid the toolbar and the Control bar
along with the panels. egui matches a chord without Shift when Shift
is also held.
A new UI command, window.panels (default Shift+Tab), shows and hides
the dock and the floating panels and leaves the toolbar and the
Control bar as they are. The shortcut table tries chords with more
modifiers first, so Tab's chord no longer takes Shift+Tab. Hiding the
panels with Tab or Shift+Tab also closes a panel popped out of the
icon column, which stayed on screen at the window's right edge.
Docs: docs/target-app-parity.md.
In Draw Inside, a double-click with the Selection, Direct Selection or
Group Selection tool outside the object being drawn into now sets the
drawing mode to Draw Normal (view.drawMode {mode: "normal"}), as
Shift+D does. The object's geometric bounds, grown by the selection
tolerance, count as inside, and a double-click after the object was
deleted also returns to Draw Normal. The tool then takes the
double-click as before: with the Selection tool, a double-click on the
empty canvas still exits isolation mode, and one on another object
still isolates that object. Other tools leave Draw Inside on.
Docs: docs/target-app-parity.md.
The Color Picker gets an eyedropper button beside the New and Original
chips, and `ui.sampleScreenColor {stroke?, picker?}` shows the system's
color sampler. The command answers at once. When the person clicks a
color anywhere on the screen, the color becomes the fill or the stroke
through paint.setFill / paint.setStroke as one undo step, or with
`picker` it becomes the open Color Picker's color, which previews on the
selection as any pick does. Escape cancels the sampler. The sampled sRGB
color goes through Cms::srgb_to_rgb, so in a Display P3 working space a
color inside sRGB shows as it did on the screen. `ui.inspect` reports
`screenSampler` while the sampler is open.
- macOS uses NSColorSampler (apps/vectorcraft/src/mac_screen_color.rs).
- The web app uses the browser's EyeDropper API where the browser has
one.
- Without a sampler (Windows, Linux, other browsers) the button is gray
and the command returns an error. Keyboard Shortcuts lists the
command, so it can be given a key.
Docs: docs/control-protocol.md, docs/target-app-parity.md.
The Control bar had no isolation controls. In isolation mode it now
shows an Exit Isolation Mode button (`object.exitIsolation`), except
while a pattern is edited: the isolation bar's Done and Cancel end
that edit. Outside isolation mode, with one group, compound path,
path, image, mesh or placed file selected, it shows an Isolate
Selected Object button (`object.isolate`). After a Direct Selection
click on a path inside a group, the button isolates that path. An
envelope's contents get no button: for them the Control bar shows the
envelope's Edit Envelope and Edit Contents buttons.
The button offers the kinds of object that the Selection tool's
double-click isolates. The double-click's check moves unchanged into
`vectorcraft_tools::select::isolates`, and both use it.
Docs: docs/target-app-parity.md.
Geometry effects on a group or layer, and effects on a symbol instance,
blend or repeat, outline the type inside it. outline_nested_text
replaced each type object with a plain group of its outlines, so the
type lost its opacity, blend mode, opacity mask, knockout shape
option, own effects and perspective, and hidden type was drawn,
exported and made visible by Expand Appearance.
outline_nested_text now replaces type with the art that as_art makes
for it, which is also what reshape makes for type with geometry
effects of its own. That art has the type's own geometry effects and
perspective applied and keeps its name, transparency, opacity mask
and other effects. as_art also keeps the object's visibility and
knockout shape option, so hidden type with a geometry effect of its
own is left out of exports. Type in a clip group's clipping path still
becomes its plain outlines and now keeps its visibility.
Duplicate Artboards, the Artboard tool's Alt-drag (artboard.move with
copy) and Paste of a copied artboard gave the new artboard the largest
artboard id plus one, saturating. In a document with an artboard id
of 4,294,967,295 (u32::MAX), which only a hand-made file has, the copy
got that same id. Artboard guides record the id of their artboard.
Deleting either of the two artboards with that id then deleted the
guides of both, and guide.list reported the copy's guides on the
other one.
push_artboard_copy now takes the id from Document::next_artboard_id,
as artboard.new and Convert to Artboards do. Since the #1718 fix, that
function returns the smallest free id when the largest is u32::MAX.
In a document whose artboard ids are all below u32::MAX, a copy gets
the same id as before.
Docs: docs/target-app-parity.md.
With Align to Artboard chosen, the Align panel's six Distribute Objects
buttons and its two Distribute Spacing buttons acted on the selection.
object.distribute and object.distributeSpacing took no to or artboard
param. Distribute Objects kept the outermost objects where they were
and did nothing with two objects, and Distribute Spacing kept the first
object's left or top edge and the last object's right or bottom edge
where they were.
Both commands now take to ("selection", "artboard" or "key") and
artboard, and the panel sends its Align To choice as to. When to is
"artboard", the first object's left or top edge goes to the artboard's
left or top edge, and the last object's right or bottom edge goes to
the artboard's right or bottom edge. Distribute Objects spaces the
other objects evenly by its button's line, and two objects are enough.
Distribute Spacing without a spacing value makes the gaps between the
objects equal. Both commands use the same artboard as object.align.
The app passes the active artboard to both commands, and recorded
actions leave it out, so an action plays on the artboard that is
active when it plays. With "selection" or "key", or without to, both
commands work as before. Distribute Spacing with a spacing value still
keeps the key object in place with each of the three to values. With
anchor points selected, both commands still space the points among
themselves and do not use to.
Docs: docs/mcp.md, docs/target-app-parity.md.
The Legacy .ai Options dialog gains Hidden Objects (Hidden Layer, Omit, Make Visible) and Text
(Editable, Outlines), translated in every catalog. Docs: type and hidden objects in the legacy
.ai paragraphs, the restored revision rows and roadmap row.
Use automatic vector smooth shading without a separate control.
Enable Cap by default and keep continuous rim curves and bounded seam joins.
Integrate the shared dialog with the other three 3D effects.
Preserve the writer and reader changes while integrating the current main.
The combined candidate passes all eight local CI gates and the 2,048-case
reader panic verification. Hosted checks remain required before landing.
Refs #1893, #1894.
A shortcut set another app saved as slash-name text (a .kys file whose
text starts with a slash name and "{", such as "/Menus {") opens in
Import Shortcut Set from File › Open, from Edit › Keyboard Shortcuts ›
Import… in the desktop app, or from a drop on the window, whether or
not a document is open. The dialog proposes a target for an entry when
an earlier import kept one for that name, or else when one tool,
command or panel matches the name clearly by its id or by one of its
labels. It decodes the file's key and modifier codes from the codes
earlier imports kept, from the example in the AiCommandPalette README
(/Key 48 for the 0 key, with /Modifiers 0 taken as no modifiers), and
from VectorCraft's default shortcuts where at least two entries vote and
four votes in five agree. The user picks the remaining targets, presses
the key for each unknown key code or marks it No equivalent, and chooses
the modifiers for each unknown modifier code. Apply assigns the ready
rows with the Keyboard Shortcuts rules: a tool takes a single key,
optionally with Shift. It goes through the rows from the last one back
and leaves a row out when a later row it applies has the same shortcut
or the same tool, command or panel. Keyboard Shortcuts then shows the
result as unsaved changes until the user clicks OK. Copy Report copies
the set's name, the rows Apply leaves out and the parts of the file that
weren't read.
- shortcutSet.read is a query command: it reads a set and does not
modify the document or the preferences. It returns each entry with
its section, codes and line, and up to 1,000 skipped tokens, each cut
to 80 characters. It reads at most 4 MiB of text and 20,000 entries,
skips blocks nested more than 16 deep and cuts names at 1,024 bytes.
With a path it reads at most 4 MiB and 4 bytes of the file, and with
--automation-read-root (#832) the path must lie inside the read root.
A file whose text starts with "<", such as an XML shortcut file saved
as UTF-8, fails with "This shortcut file is in a form VectorCraft
can't read."
- importMatches.list, importMatches.set and importMatches.reset read
and change the matches kept with the preferences; resetting the
preferences keeps them. With Remember My Choices on (the default),
Apply keeps the targets and codes the user chose, and for the rows it
assigns, it also keeps the matched targets and the codes decoded by
agreement. A later import proposes them.
- The name matcher compares a name with ids by exact match and shared
words. It compares the name with labels, in English and in the
interface language, by exact match, shared words and character
pairs. It proposes a match only when one tool, command or panel
scores at least 0.6 and at least 0.1 more than the next one.
- When Import Shortcut Set is opened from Keyboard Shortcuts, Cancel,
Escape and ui.dialog.cancel close it and return to Keyboard Shortcuts
as it was (dialogs::PARENT). A document's queued Missing Fonts dialog
(#792) opens after Keyboard Shortcuts closes.
- File › Open has a "Shortcut sets" filter, and in the desktop app
Keyboard Shortcuts' Import… shows .json and .kys files.
Docs: docs/mcp.md, docs/control-protocol.md, docs/development.md,
docs/target-app-parity.md, docs/roadmap.md.
With Separations Preview on, the renderer copied every node of the
document in every frame and separated each of its colors again. With
Overprint Preview on, it copied each object with an overprinting fill
or stroke in every frame, with the groups and layers that hold it. The
copies were new Arcs, so the geometry, stroke outline and raster
caches missed for each of them in every frame. In a release build,
drawing a 2880x1800 frame of a 50,000-path document again with the
same view and Separations Preview took 795 to 908 ms.
The renderer now keeps the art it prepared and what it was made from,
and in the next frame it uses that art again for each layer, symbol
art and member that has not changed. Only the objects that changed are
prepared again, with the layers and groups that hold them. All of the
art is prepared again when Discard White Overprint changes and, with
Separations Preview, when the plates shown, the proof setup, the
swatches, the Spot Colors option or the color settings change or a
profile is registered (cms::profiles_generation). The renderer now
draws that frame again in 98 to 107 ms, with the same pixels.
Docs: docs/target-app-parity.md.
In the web build, now_ms() in crates/ui-egui/src/lib.rs returned 0.0.
The status bar read "render 0.0 ms · ui 0.0 ms" however long a frame
took, and the Layers panel's budget of 8 ms for the row thumbnails it
renders in a frame never ran out, so only its limit of 16 thumbnails
applied.
now_ms() now returns the milliseconds since its first call, read from
the browser's performance.now() through web-time, which the build
already has through eframe. The desktop clock is unchanged. In Chrome,
after a pan frame of 750 ms over a document of 50,000 paths, the status
bar now reads "render 750.7 ms". The Layers panel now renders the
thumbnails of 24 groups of 2,000 paths one per frame, where main
rendered nine of them in one frame of 217 ms. The canvas has no render
worker in the web build, so it still renders on the UI thread and draws
the same pixels.
Docs: docs/target-app-parity.md.
The "Performance unverified" bullet in docs/target-app-parity.md said
that the budgets had not been run on an idle machine since 2026-10-01,
that the last loaded run missed the 16 ms frame budget at 50k paths,
and that no user had reported slowness in mainstream work beyond #575
and #758. On 2026-10-11 #1155 recorded idle-machine runs, made before
#1173 merged, and runs of #1173's change met every budget at 50k paths
(load average 4 to 7) and at 25k paths. #1097 (zoom and pan in 0.9.0)
and #1731 (a drag that stutters as it starts) are open. G15 in
docs/gaps.md called the 25k-path run the last one, but on 2026-10-13 a
comment on #1172 reported runs of 0.10.0 on Linux with 4 logical CPUs
that missed all three render budgets at 20k paths.
The bullet now gives the 25k-path run and the runs in #1172, and lists
#1097 and #1731 with #575 and #758 as open user reports of slowness.
G15 no longer calls the 25k-path run the last one and adds the runs in
#1172.
With Type › Show Hidden Characters on, the canvas laid out every
visible text object in view in every frame to place the dots on
spaces and the ¶ and # marks. With 1,000 two-line labels in view,
drawing the marks took a median of 19.7 ms per frame in a release
build.
The overlay now keeps each text object's marks in document space,
together with the node it laid them out from. In a frame it lays out
a text object only when its node is a new `Arc` (an edit makes one)
or the frame before did not draw it, and it lays out all type in view
again when the fonts or the hyphenation exceptions change. The marks
are still placed for the current view in every frame. With the same
labels, drawing the marks takes 0.8 ms per frame, and the shapes drawn
are the same as before.
Docs: docs/target-app-parity.md.
When the last frame took more than 8 ms to render, the desktop app's
canvas sends the next frame to the render worker and draws the last
one until the new one arrives. After a pan or a zoom it draws the last
frame moved and scaled. After the view angle changed, it drew no art
until a frame at the new angle arrived. The angle changes during a
Rotate View drag and when the status bar's angle menu, Reset Rotate
View or a saved view sets it. In a headless run of the canvas on a
document of 50,000 paths, none of the 60 frames of a turn from 0° to
90° drew any art.
The canvas now draws the last frame as a mesh turned into the new
view. Each corner goes where the new view puts the document point that
the corner showed. The frame at the new angle replaces it when it
arrives, as before. Pixel Preview's frames already turned with the
view and are drawn as before.
Docs: docs/target-app-parity.md.
Only type at full opacity, in Normal blending and without object
effects took its glyph outlines from the geometry the renderer keeps
for each type object. Type at less than 100% opacity or in a blend
mode, and the art drawn for type with a drop shadow, glow, blur or
other raster effect, were laid out again in every frame.
draw_text now takes the geometry from text_geom_of when the type it
draws is fx_owner, the node draw_arc is drawing, and the frame does
not snap type to pixels. While the effect-art cache keeps type art,
that art is drawn as fx_owner. The geometry is laid out again when the
type is edited or the fonts change, and frames with Type Optimized
anti-aliasing lay type out as before. On 8 threads at 2880 x 1800, a
page of 1,000 type objects at 80% opacity takes 13 ms a frame instead
of 23 ms, with the same pixels.
Opaque type kept the layout of earlier frames when a symbol it shows
as an inline graphic changed size and the document's inline graphics
had not been resolved again. text_geom_of now lays type out without
the cache when the text it is given is not the node's own, such as the
copy that Document::inline_resolved makes with the new size.
Docs: docs/target-app-parity.md.
For a name that is neither a loaded nor an installed family,
FontDb::canonical built the list of names of every loaded face on
every call. Layout asks for a style's face for each span it shapes
and three more times per paragraph, so the time to lay out type set
in a font that is not installed grew with the number of fonts loaded:
a story of 1,000 paragraphs took a median of 409 to 507 ms in a
release build with 400 fonts added.
The database now keeps canonical's answer for each such name and style
until fonts are added or rescanned, and it keeps at most 4,096
answers. The same story takes a median of 56 to 71 ms, and canonical
gives the same answers as before.
Docs: docs/target-app-parity.md.