With the macOS menu bar, File › Scripts was read only at startup and
when the window became active, so a script added while the window
stayed active did not appear. The native menu now reports a menu
opening (Event::Opened, when AppKit begins tracking), and
native_menu::run reads the folder again, as opening the in-window File
menu does. Menu updates wait until tracking ends, so the new listing
shows the next time the menu opens.
Docs: docs/mcp.md (File › Scripts), docs/target-app-parity.md.
longPagesInPoints now applies only to files without a standard. PDF/A-2 limits page
boundaries to 14,400 units (ISO 19005-2, 6.1.13), so a PDF/A file with a page longer than
200 in keeps its UserUnit whether the option is on or off. The Save PDF dialog disables the
checkbox for PDF/A as it does for PDF/X.
Follows #6643.
The docs said every livePaint key left out is reported as missing. livePaint.info reads `id` when `group` is left out or null, and reports a missing group only when both are left out.
- drawn_alike ignores kerning_before, so a space before the "f" of "fi"
no longer splits the ligature.
- SVG export takes a line's starting space from kerningBefore and the em
shaping sets the character in (vectorcraft_text::line_start_space),
not from where the glyph is drawn, which can include a shaping offset;
the reader layout for justified lines leaves the space out, so those
lines start there too.
- Type on a path writes the space as the first character's dx; a space
after a line break, which <textPath> can't write, is outlined.
Release Type on a Path copied the stored runs into point type with the
Dynamic Text flag still set, so the text dropped back to its stored size
and the file saved the flag on point type. The point type now gets the
drawn sizes and the flag is turned off.
The legacy .ai writer writes Dynamic Text type at its drawn sizes; it
now also warns that the setting itself is not written, as the editing
data writer does, whether the type is written as text or as outlines.
docs/target-app-parity.md lists reshaping or scaling the path among
VectorCraft's own rules, and a task bar comment marks leaving the item
out for threaded type as VectorCraft's own rule.
When plugin.install writes a module that lists permissions, its grant
can't be written, and the old module can't be put back (or, on a first
install, the new module can't be removed), the store holds the new
module. install now takes put_back_plugin's result, so the plug-in's
disabled entries (by id and by stored file) are cleared as for any
install the store holds. The old grant, or none, still doesn't cover the
new module, so the next start skips it until it is approved.
Docs: docs/plugins.md, docs/mcp.md, docs/target-app-parity.md.
Commands that need an object id read it with `id_param`, which returns
None for a value that is not a non-negative integer, so `{"id": "5"}` or
`{"id": -1}` was refused with "missing id". They now read it with
`object_param`, which refuses such a value with "`id` must be a whole
number", naming the key. This covers `document.node`, the `path.*` anchor
and handle commands, the `text.*` range and path commands, the mesh,
envelope and slice point commands, `layer.target`, `layer.move`,
`node.move`, `select.magicWand`, and the `source`, `target`, `group` and
`path` keys of `appearance.transfer`, `appearance.copyFrom`, the
`livePaint.*` commands and `text.createInPath`.
The optional `group` of `livePaint.fill` and `parent` of `node.move` are
refused the same way when given. Before, a malformed `parent` moved the
object to the top level and a malformed `group` fell back to the
selection.
With Envelope Options > Distort Appearance on, VectorCraft turned the strokes
of type in an envelope into filled outlines and bent them with the art, so a
stroke got thinner where the grid squeezes the type and thicker where it
stretches it. The drawn art of users' .ai files keeps those strokes as stroked
paths at the type's own weights, and the ring they paint is as wide wherever
the grid squeezes or stretches the type. An envelope of stroked type in such a
grid therefore didn't draw as its drawn art and kept it instead of opening live.
Type's strokes (its characters' and those on its own appearance) now paint
along the bent glyph outlines at their own weight, with Distort Appearance on
or off, also in an envelope inside an envelope:
- doc: `map_node` maps the groups that hold type's glyph outlines with
`Hooks::type_appearance` in place of `Hooks::appearance`, for type in the
envelope's contents and for type that a live object in them (an inner
envelope) outlined (`Warper::type_outlines`). Such a group's opacity mask
is mapped with the ordinary hooks, so it bends as other art does. Groups
marked as type's outlines in the contents themselves (art an earlier Expand
made) stay plain art.
- effects: `bake_type_appearance`, `bake_appearance` with strokes kept as
strokes: geometry effects on type's fills and strokes still bake in and bend.
- doc: `max_outset` counts type's character strokes as type's own visual
bounds do (`char_stroke_outset`). It left them out, though they already
painted past an envelope's grid where the glyphs reach its edge; this also
grows the visual bounds of blends and repeats of stroked type and the reach
of symbol art holding it.
Paths' strokes, geometry effects, opacity masks, linear gradients and pattern
fills bend as before. The canvas, hit testing, effect bounds, Expand, every
export and the .ai save take the evaluation from the same hooks. Expand of an
envelope of stroked type gives the bent outlines with the strokes on them.
This changes how VectorCraft's own envelopes of stroked type draw.
Tests: envelope_distort::type_strokes_paint_at_their_own_weight_in_a_squeezed_envelope,
a_stroke_on_types_appearance_paints_at_its_own_weight_in_a_squeezed_envelope,
type_strokes_paint_at_their_own_weight_in_an_envelope_inside_an_envelope,
a_paths_stroke_still_bends_in_a_squeezed_envelope,
an_opacity_mask_on_type_bends_as_other_art,
the_visual_bounds_of_an_envelope_hold_its_types_strokes,
the_visual_bounds_of_a_repeat_of_stroked_type_grow_by_half_the_stroke,
expanding_an_envelope_of_stroked_type_keeps_the_stroke_at_its_weight,
bake::tests::type_pieces_bake_their_geometry_effects_and_keep_their_strokes and
tests_native::an_envelope_of_stroked_type_whose_drawn_art_strokes_the_bent_outlines_at_the_weight_opens_live.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At the Type tool's caret, kerning goes on the character before the caret.
At the start of the text there is none, so Alt+arrows, the Kerning field
and Reset Tracking and Kerning did nothing; at a later paragraph's start
the kerning landed on the invisible line break.
CharStyle::kerning_before (kerningBefore) is the space before a character
that starts a line by itself: the first character of the text, of a
paragraph, or after a forced line break. Shaping gives its cluster the
space and draws it that much later along the line (right to left the
space is at its right; in vertical type it moves down the column). The
caret at such a line's start sets it (edit::caret_kerning_target), in
type.step, type.resetTracking and the Character panel; text.setRangeStyle
takes it. SVG export starts the line where the character is drawn. The
Eyedropper and character styles leave it alone.
A light filter of plug-in API level 2 with the `progress` permission ran
on the UI thread, so its progress reports and `vc_cancelled` never
reached a progress window. It now runs in the background under its light
budget, as a heavy filter does, when it runs at once from Object ›
Plug-ins or its shortcut, or when OK runs it with Preview off. The
progress window shows its share and Cancel stops it. With Preview on, OK
keeps the previewed run. A filter that lists a `commandAllowlist` still
runs on the UI thread.
Docs: docs/plugins.md, docs/mcp.md, docs/target-app-parity.md.
arboard's Wayland data-control read waits for the app that owns the
clipboard to send its data with no time limit, and its X11 read waits up
to 4 s for each format it tries. These reads ran on the UI thread: a
Copy or Cut checking what the clipboard held, a paste, and the paste
check once its background thread is gone. A clipboard owner that never
sent its data froze the window.
On Linux and the BSDs, arboard's reads now run on a worker thread with a
clipboard handle of its own. Text and file lists wait 2 s at most and a
bitmap 10 s; the core-protocol fallback's text reads already wait 2 s.
While a read is unanswered, later reads fail at once. Writes stay on the
UI thread's handle.
The core-protocol fallback's worker is now generic and built on every
platform. On X11, closing the clipboard within 25 s of a read that got
no answer waits for that read to end, so the app's own handle is the
last one and hands the copy to the clipboard manager. On data-control
and the core protocol, closing does not wait for an unanswered read.
A toolbar made with New Toolbar was filled only by dragging tools from
its All Tools drawer, which the ••• button opens. It now also has a +
button (Add Tool…) above the ••• button. Its menu lists every tool under
the drawer's headings; a click adds the tool at the end of that toolbar
(toolbar.addTool with toolbar), and the tools already in it are dimmed.
Under Remove Tool the menu lists the toolbar's tools, and a click takes
one out (toolbar.removeTool). The button takes the keyboard focus, and
the menu scrolls inside the window like the other long menus.
The drawer and its drags work as before. Add Tool… and Remove Tool have
rows in every catalog.
The instruction, time and memory budgets and the output size a plug-in
run can exceed had @msg rows only in hi and sv. Every other complete-message
catalog now has them, next to the other plug-in budget rows, and every
complete-message catalog has a row for the error of a plug-in removal that
leaves the plug-in's grant in the plug-in store.
The message scan now reads a message that an error or a status takes from a
&str constant (Error::Limit(TIME_BUDGET.into()),
crate::runtime::SELECTION_TOO_LARGE), so the untranslated-strings report
lists such messages. Tests: message_scan_reads_messages_held_in_constants
and plugin_limit_errors_are_translated.
Follows #6608. ui.inspect shows a library loaded from outside the read
roots as loaded/<file name>. A request may now name the library or its
panel by that id in a panel, library or panels param, in a docking
destination (anchor, before, onto, above, below) and in
window.panel.layout's action: ui.set {panel} reopens the closed panel
instead of floating a new one. The reply to such a request, an error
included, shows the shown id.
ui.inspect rewrites only whole ids, and only in the fields that hold
library and panel ids, so a readable library's id that begins with a
hidden one keeps its path, and status text, the command palette query
and a script's report keep their text. A hidden library does not take
the id of a library loaded without a path.
ui.inspect also leaves out of recorded action steps the path and folder
params, params named like destinationFolder and the entries of a paths
list that lie outside the read roots, also inside a batch step's
commands. It hides a library id holding a path written with either
separator, and checks a folder preference against the roots as written,
without trimming spaces.
A script whose first lines name an engine with `#targetengine name` (or
`//@targetengine name`) runs in that engine, and a later script that names
it sees the globals and functions earlier runs left, until the engine is
dropped or the session ends. A script without the line, and
`#targetengine main`, still get a new engine for each run.
- A session keeps at most 8 named engines, each with a 64 MiB heap. A run
that goes past the heap ends with the heap limit, and the engine stays
usable. Names are 1 to 64 bytes. The line comes once, in the script
itself, before its code and its #include lines.
- An uncaught error, a limit or script.stop puts the documents back and
keeps the globals the run set. An internal error drops the engine. A run
stopped at a dialog or left paused ends where it was before the engine
is kept. An error in a function an earlier run defined is reported
without a line and column.
- `vc`, `$`, `alert`, `confirm` and `prompt` are made once for each engine
and act for the run that calls them, including copies an earlier run kept
in a global. `vc.args` gives a read-only copy of the calling run's
`args`, and what a run sets on `$` lasts for that run only.
- `script.engines` lists the named engines with their heap, and
`script.engine.drop {name}` drops one. Scripts cannot run either.
- The engines stay on the thread that made them. A session used on
another thread starts there with no named engines, and the old ones are
dropped on their own thread.
- The web build has no named engines: a run that names one other than
`main` ends with an error before any of it runs.
- `Realm::end` ends a run where it is, `Realm::error_source` counts from
the realm's last script, and `HostItem::FrozenGetter` gives a frozen
value each time it is read.
- The script checker lists `#targetengine` as a supported directive.
Docs: docs/mcp.md, docs/target-app-parity.md, docs/gaps.md.
The plug-in approval dialog's OK stays disabled until the question has
been on screen in the focused window for 0.5 s. A click, Enter or Space
before then, a press begun before then, and a direct confirm install
nothing and leave the dialog open. The wait starts over for every new
question, even one that reads the same, and when the window gets the
focus back, so a question swapped in just before a click lands is not
approved by that click. Cancel and Escape work at once.
An error a finally block passed on unchanged was reported at the end of
that block, in script.run's report and in the Script Console, because the
block's end raised the error again at a new site. The interpreter now keeps
the site of the exception each finally block runs for, keyed by the stack
slot that holds it, and the block's end throws the exception on from that
site. This holds through nested finally blocks, called functions, resumed
runs and marked host errors. An error the finally block throws is reported
at that throw, and one its return, break or continue discards is not
reported.
Type › Dynamic Text (`type.dynamicText`, also on the Contextual Task Bar
for type on a path) scales the text by one factor so it fills the span
between its start and end brackets, and the layout fits it again after
each edit to the text, the path or a bracket. The first character's
size moves in 0.1 pt steps within 0.1 to 1,296 pt. Turning it off, or
threading the type, keeps the sizes it was drawn at.
- The model saves `dynamicText` only when on.
- SVG, the legacy .ai writer and the .ai editing data write the drawn
sizes through `vectorcraft_text::drawn`; the editing data warns that
the setting is not written.
- Shrink Text to Fit and Dynamic Text step the size the Character panel
shows when a size measures the cap height, x-height or ICF box.
Docs: docs/mcp.md, docs/type-parity.md, docs/target-app-parity.md.
Part of #4134.
vectorcraft_plugins exports FUEL_SLICE and BYTES_COPIED_PER_FUEL (hidden
from the docs), and the bulk memory fuzz cases use them for the passes
around one slice, the lengths around one unit of fuel and the fuel
estimate, in place of the copied 20,000,000 and 64.
The Save PDF dialog's General section gets "Write pages longer than 200 in
at full size in points" (longPagesInPoints in document.exportPdf,
document.export, presets and document.pdfSettings), off by default. When it
is on, a page with a side longer than 14,400 pt is written with its media
box in points and no /UserUnit at every PDF version, and a PDF 1.5 file
stays PDF 1.5. The export's warning for such a page then says it is written
in points. With the option off, and for pages of 14,400 pt or less, the
file is the same as before.
Docs: docs/mcp.md, docs/target-app-parity.md. The new label has a row in
every catalog that lists the dialog's other options.
The status message already ends in an ellipsis and shows its full text
on hover when it is clipped. The Help Bar's hint and the tool name in
the status bar ran past the window edge instead; the Dimension tool's
hint is wider than an 800-point window. Both now end in an ellipsis and
show their full text in a tooltip on hover. Screen readers receive the
full text, and text that fits shows no tooltip.
In a narrow status bar the tool name now ends in an ellipsis before it
takes the status message's room. Before, the tool name could leave the
message almost no width.
The tooltips of the clipped hint, tool name and message wrap to fit
inside the window. egui's tooltips are 500 points wide, which is wider
than an 800-point window at UI Scaling 1.6 or more.
The hint and tool name tests fail without the change. The status
message test covers its existing ellipsis and tooltip.
Ruler guides were a list kept beside the layers. Each guide is now an
object of its layer or sublayer, always directly in it and above the
layer's art. These behave as on main:
- Guides draw on screen only. Exports (SVG, PDF, PNG, PSD), Fit All,
Select All, the art bounds, clicks and marquees, and a layer's
Pathfinder and Transform effects leave them out. The Layers panel
lists them as guide rows of their layer, the highest guide.list
index first.
- Deleting a layer deletes its guides, and merging layers keeps their
guides in the merged layer.
- The guide commands number guides as guide.list does, in the order
they were made.
New: each guide takes an id from the document's object ids. An object
command given a guide's id fails with an error, and node.move cannot
put a guide into a group or out of its layers. A guide made while an
opacity mask, a symbol or a pattern is edited moves to the top
visible, unlocked layer when editing ends.
File format: native files still save the guides in the document's
guides list, in every file version, so older builds read them. Each
entry may now also carry the guide's id, name, locked and hidden
flags; older builds ignore those fields. Opening a file puts each
saved guide on its layer (on the top visible, unlocked layer when the
file names none or a layer it does not have), and each saved guide
counts against the object limit for opening a file. A ruler guide node
that a file holds below a layer's art moves up to the layer's guides,
and one anywhere else (in a group or another object, beside the
layers, or in a symbol) is removed. Ruler guides read from an .ai file
become guide objects on the layer they were on, numbered in the order
the file lists them.
The params docs now state the direction (and the zero) of each angle or
rotation a command takes; behaviour is unchanged. Shape creation, text.create,
the grids, add-on shapes, saved views, Liquify, Puppet Warp pins, Mirror and
Radial Repeat and Vector Halftone turn clockwise; text character rotation,
gradients, the eraser tip and print screen angles turn counter-clockwise.
view.saved.new's rotation note changes its schema, so the API level goes to 3.
A test walks every command's params doc and fails when a param named like an
angle or rotation doesn't say clockwise or counter-clockwise.
Check Script read names with Rust's alphabetic and alphanumeric tests, so it
split names at U+2118, U+203F, U+00B7 and the zero width non-joiner and joiner,
and took U+00B2 and U+2E2F into names. It now uses the Unicode 16.0 ID_Start
and ID_Continue rule that vectorcraft-js uses, with `$` and `_`
(https://www.unicode.org/reports/tr31/,
https://tc39.es/ecma262/#sec-names-and-keywords).
A `\uXXXX` escape is part of a name only when it stands for a character the
name may hold where it is: `\u0030abc` no longer becomes `0abc`, and malformed
escapes end the name.
The engine's lexer comparison test now checks that both crates take the same
characters into names, written out and as escapes.