When the desktop app opened a document on a thread of its own, that
thread parsed it, but open_read, which the app ran on the UI thread,
read the document's linked files, decoding BMP, PSD and RGB TIFF files
and storing them again as PNG, and laid out the type. In release builds,
a document with 12 linked 3000 x 2000 TIFFs kept the UI thread busy for
1.04 s, and one with 300 Auto Size area type objects, every second one
in three columns, kept it busy for 0.85 s.
read_for_open now reads the linked files and lays out the type on the
opening thread, with the path and the Update Links preference that
opening::start passes it, and open_read only adds the finished document.
open_read now takes 0.02 ms and 0.1 ms on those documents. For the
measured documents, the opened documents, results and undo states are
the same as on main.
Docs: docs/target-app-parity.md.
Every symbol instance was measured as its transform applied to the
20 × 20 square around the origin, which symbols made in the app keep
their art in. The SVG and DXF importers keep symbol art as drawn, so
the box of an instance of an imported symbol did not match its art.
The SVG Star instance at x 20, y 30 draws from (20, 30) to (40, 50)
and was measured from (10, 20) to (30, 40). Align moved such an
instance by the wrong amount, clicks and marquees beside its art
selected it, and the canvas, exports and prints left it out when its
art reached the view or the page and the square did not.
An instance is now measured by its symbol's art, which
Document::symbol_boxes looks up in the document each time
(Node::geometric_bounds_with, visual_bounds_with and
reach_bounds_with). With Use Preview Bounds on, the strokes in the art
count at their own weight, as the canvas draws them, and
object.setBounds counts them at that weight when it sizes an instance
or an object that holds one. When strokes do not scale and a width or
height of 0 or more is smaller than what a path's stroke adds,
object.setBounds now gives the path no width or height, as it does
for an instance whose art has strokes; before, it mirrored the path.
With Use Preview Bounds on, object.setBounds now measures the margins
that strokes add object by object, the members of groups and the
objects in symbol instances' art included, and solves for the width,
height and reference point asked for when the width and height are 0
or more. Before, it measured the margins of the whole selection and
could miss the size or the reference point asked for when strokes did
not scale and the objects' strokes added different margins, and when
strokes scaled and the width and height were out of proportion, even
for one path. A clip group, a Shaper Group, a compound path, type, and
a group or instance with effects or an opacity mask of its own are each
measured as one object, and so is an instance in perspective or one
turned or slanted relative to the bounding box, other than by quarter
turns. When the selection holds more than 4,096 objects to measure this
way, each selected object is measured as one. Instances inside symbol
art are measured by their own symbol's art. An instance is measured as
the square when its symbol is missing, when its symbol or a symbol
nested in its art holds an instance of itself, and when symbols nest 32
deep in its art.
Align and Distribute, the Transform panel, the bounding box and the
selection outline, clicks, the marquee, document.inspect and
document.node, Fit to Artwork Bounds and Fit to Selected Art, canvas
culling, thumbnails, the SVG filter region of an instance with a
raster effect, raster effect images, Export Selection, the SVG of an
artboard, printing, and the PDF, EPS, EMF/WMF and DXF writers use the
new box. The renderer also culls an instance by the effects and
brushed strokes in its art, and it culls a blend, envelope or repeat
by the brush art of what it holds and the effects in the art of the
instances in it. It culls an instance with geometry effects of its own
by the effects in its art after those geometry effects reshape the art.
It measures the effects in a symbol's art once for each symbol and
linear part of the transform that places it. For symbols whose art
holds instances, it measures at most 4,096 placements a frame, and at
most 4,096 more for each pattern tile it rasterizes and each placed
document it draws. Past that, it estimates the reach from the symbol's
reach with no transform, which it measures once. When the drawn
document's symbols change, the renderer drops the cached bounds of the
instances and of the objects that hold them.
The PDF and EPS writers, the SVG of an artboard and Skip Blank
Artboards grow the box of an instance whose art holds brushed strokes
by 16 times the widest stroke (at least 1 pt) of the objects in that
art that have a brushed stroke, plus 8 pt, as the renderer grows the
cull bounds of a brushed path. They keep an instance whose brush art
reaches the page. The SVG of an artboard also keeps an object that
holds an instance when what the canvas paints for it, the effects in
the symbol's art and the instance's own included, reaches the artboard.
Not yet: other commands that measure objects, such as Select › All on
Active Artboard, Transform Each and Convert to Artboards, still measure
an instance by the square.
Docs: docs/mcp.md, docs/target-app-parity.md.
The renderer kept the evaluation of each blend, envelope and repeat
while the object and a hash stayed the same: the addresses of the art of
the symbols the object showed and of the patterns it painted with, and
the patterns' tile bounds. When anything else the evaluation reads
changed, such as a pattern's tile type, symbols in symbol art, the
fonts or the color settings, a renderer that had drawn the object went
on drawing the old evaluation. The quads of a gradient mesh were kept
the same way after a change to the color settings, and the art drawn
for a live object with an effect of its own was kept by the object's
address alone.
resources_key now also walks the opacity-mask, symbol and pattern art
it finds. It hashes what it finds, the font generation when it finds
type or a placed document, and the color settings. Mesh quads are made
again when the color settings change. The art drawn for an object's
effects is made again when the hash changes, if it was made from
evaluated live objects.
With other builds running, a temporary release test timed the hash for
400 live objects at 17 to 506 microseconds a frame, against 10 to 16
before.
Docs: docs/target-app-parity.md, docs/gaps.md.
General > Use Precise Cursors already worked and is saved with the other
preferences. This adds a `view.preciseCursors` command that toggles that same
preference, so it can be given a shortcut in Edit > Keyboard Shortcuts. It has
no default shortcut and no View menu item.
The comment in canvas.rs claimed Caps Lock gave precise cursors; nothing reads
Caps Lock (egui has no state for it), so it now describes what the code does.
Tests: for the tools whose own pointer the preference replaces (Pen, the anchor
tools, Curvature, Mesh, Eyedropper, Slice, Blend, Shape Builder) the pointer is
their own when off and a crosshair when on; every state of the Pen's pointer
turns into a crosshair; the preference round-trips through
to_json/apply_prefs; the command toggles it, updates the saved UI state, is
listed in the shortcut editor without a key and can be assigned one; switching
it creates no undo step and leaves the document saved; with system cursors the
glyph fallback is the system crosshair either way. The new label is translated
in the languages that claim complete menus.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Covers the Illustrator behaviour that a corner placed after a smooth anchor
has no handles, so the segment is shaped by the previous outgoing handle alone.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
text.create, text.createInPath and the MCP add_text tool now take a
`justify` (auto|left|center|right|justifyAll, as text.setStyle). Point
type already laid out centred or right-aligned text around its origin;
it just could not be made that way, so scripts measured the bounds and
moved the text. Changing it afterwards stays text.setStyle {justify}.
Expand had nothing for live repeats, so a radial, grid or mirror repeat stayed
live. Its Object option now turns every repeat in the selection (inside groups
too, inner ones first) into a group of its instances, keeping the repeat's id
and place, in the same undo step; the dialog enables Object for a repeat.
Expand Repeat shares the code.
The typed [ and ] now reach the Blob Brush, the Eraser and the Symbolism
tools, whose bracket keys are in the public shortcut lists. A size of the
Paintbrush's own (and path.freehand's scale) is left out until its
behaviour is decided: #1661 stays open for that.
The tools took `[` and `]` (the Blob Brush's and the Eraser's size, the
Symbolism tools' diameter and, with Shift, intensity), but the app never
passed the keys on: only `ui.key` over MCP reached them.
- The keyboard handler gives a typed `[`, `]`, `{` or `}` (Shift) to the
active tool when it claims the key. It reads the typed character, so
layouts where a bracket takes Option or AltGr (Option-8 on a Turkish
Mac) work too. A field with the keyboard, the Type tool and Cmd/Ctrl
shortcuts are left as they were.
- The Paintbrush has a size of its own, 10 to start as the Blob Brush's:
`[` and `]` take a point off or add one, and the next stroke is drawn
at size / 10 of the stroke weight (path.freehand `scale`). Public
references describe the keys as changing the size the Paintbrush draws
with, before drawing, the brush definition left as it is; so the
stroke weight new art takes stays, and the selected path keeps its
stroke. Every brush type, as the report and the Tuts+ guide describe;
the step is our own.
The Pencil gave every path a black stroke when the stroke was None. With Fill
new pencil strokes on and a fill set, the path now keeps no stroke; only a path
that would show nothing (no fill, no stroke) still gets black.
Escape still ends editing (an empty new point type is discarded) and the type
stays selected; the Type tool now claims Escape while editing and asks for the
Selection tool, so tool shortcuts work again without the mouse.
A fully selected object is deleted along with partial anchors on other
paths. Several anchors on one closed path are removed by their original
indexes. What remains of a path stays selected, so a second Delete
removes it.
VectorCraft does not run scripts written for the reference app, and
File › Scripts held only the disabled Other Script…. File › Scripts ›
Check Script… picks a .jsx, .jsxinc or .jsxbin file, reads it without
running any of it, and shows a report: what the script needs (the
script language's own objects, the document model, files, menu
commands, dialogs, or features only the reference app has), what it
would do outside the document, the menu-command strings it passes, the
document-model member chains and type names it uses, its directives,
the files it includes, and the problems that would stop it loading.
Copy Report copies the report as JSON. File › Open and drag-and-drop
show the report for those files too.
- vectorcraft-scriptcheck, a new Standalone crate with no
dependencies, holds the checker: a lexer for ECMAScript 3 with E4X
literals and the #name and //@name directive lines, and an analysis
of the tokens that follows variables, function scopes and the files
a script includes. It never recurses: open brackets, the levels of
an XML literal and the function bodies being read are kept on stacks
capped at 4,096. It reads at most 8 MiB of a file and 32 MiB with the
included files, follows 64 included files 16 deep, looks up at most
1,024 paths for them, keeps 1,000 entries in a list, tracks 1,000
variables holding member chains and 1,000 holding File objects, and
cuts echoed text to 120 characters. A compiled script is reported as
compiled and isn't read.
- script.check (engine, a query) takes path, data or dataBase64. With a
path and includes: true, it reads the files the #include lines name
from inside the script's folder, through the automation read root,
and when the script or one of those files assigns a member chain or a
File object to a variable, the check treats the variable as holding
it in every file. It doesn't follow absolute names, names, folders or
links that lead out of the folder, names or folders longer than 4,096
bytes, or JSINCLUDE, and it reads only regular files.
- ui.checkScript shows the report (dialog scriptCheck). File › Open
lists Scripts (.jsx, .jsxinc, .jsxbin) and sends a script's path to
script.check, which reads at most 8 MiB of it.
The language rules come from ECMA-262 3rd edition and ECMA-357 2nd
edition. The directive, platform and document-model root names come
from the public JavaScript Tools Guide and scripting guide pages that
catalog.rs cites by URL, and executeMenuCommand, doScript, loadAction,
unloadAction and selectTool come from the public forum posts cited
there; only names are used. Every script in the tests is written from
scratch with invented names.
Docs: docs/mcp.md, docs/target-app-parity.md.
document.open (and file.newFromTemplate) with a path relative to the
working directory took the SVG's folder from Path::parent(), which is
"" for a bare file name and was then dropped, so relatively linked
images were "not found" (and, linked by their bare name, not listed in
missingLinks either). A relative path is now made absolute first: its
folder is the working directory, as for the absolute path.
vectorcraft-cli convert printed only the export's warnings; what reading
the input left out now comes as importWarnings.
Fixes#1604
A character style's OpenType features took nine tags. OtFeatures now
also keeps ss01-ss20, salt, titl, pnum, lnum, sups and subs (off by
default, one bit each in `extra`): they are accepted by the type
commands, stored with the style and passed to the shaper, so a font
that has them uses them.
The Affinity import test used ss01 as its example of a feature kept but
not rendered; it now uses cv01 for that (character variants still
aren't rendered) and keeps ss01 as one that is.
Fixes#1607
Lock › All Artwork Above and Hide › All Artwork Above went through the
objects above each selected object in its top-level layer, one selected
object at a time. For each object they looked up in a list whether it
was selected or already found, and they computed its bounds again for
each selected object below it until one overlapped it. With 100 tiles
selected under 25,000 rectangles, a release build of vectorcraft-cli
took 6.4 s for Lock, and with all of 5,001 objects selected it took
8.8 s and locked nothing.
The commands now go through each top-level layer that holds a selected
object once, and check each object that is not selected against the
bounds of the selected objects below it. The selected objects and the
objects found are kept in sets, and each object's bounds are computed
once. The objects they lock or hide, the count, the selection and the
undo step are the same as before. On the same documents Lock now takes
under 0.1 s.
Docs: docs/target-app-parity.md.
Gradients (params `gradients`, a checkbox in the panel for Grayscale and Color;
off unless asked for): an area whose colour changes evenly along one direction
is traced as one path filled with a linear gradient, not as bands of flat colour.
Gradients are found before the palette is made, so no colour is spent on their
bands: a connected run of smoothly changing pixels is a gradient when a plane
fits each colour channel with all three sloping the same way, the colours along
that way are at most six stops with ramps between them, and painting them back
gives the area's own pixels. Flat colour, soft edges between flat colours and
shading that goes two ways are left as they were.
The harness gains two linear-gradient inputs with known lines and stops, and
measures whether each is found, the angle of its line and the colours of its ramp.
Auto Grouping (params `autoGrouping`, a checkbox in the panel; off unless asked
for): each traced path that has others inside its outer contour comes out in a
group with them, the path first, nested as deep as the picture is. The path that
covers the whole image is nobody's group.
The grouped result paints the same picture: where paths can overlap (stacked
layers, Flat Logo, strokes) a path only joins a path below it, and only if that
moves it under nothing that shares any of its box.
The harness measures three presets with the option on ("grouped %"), painting
them in the grouped order.
With paths selected but every offset empty (an inset larger than the
shape), Offset Path failed with "select paths", sending the user to
look for a selection problem. It now says the offset leaves nothing of
the selected paths; "select paths" stays for selections without paths.
Fixes#1590
Commands whose description takes `ids` (operate on these objects;
default: the selection) failed with "nothing selected" when objects
were given but none was selected: execute checked their `enabled`
predicate against the selection, with an exception for edit.clear only.
Of the 220 commands mentioning ids, 48 were refused so; their run
already used the ids. execute now evaluates `enabled` with the given
ids selected and puts the selection back, so each predicate keeps its
meaning (Make with Top Object still needs two objects).
Fixes#1575
The public Pathfinder documentation says Trim and Crop remove any
strokes, and Merge removes them and merges same-coloured fills. The
pieces kept every stroke, and Merge, grouping by the whole style, left
same-filled objects with different strokes apart. The three now drop
stroke layers before styles are compared.
Fixes#1570
Shapes (params `shapes`, a checkbox in the panel; off unless asked for): a filled
outline that is a circle, an ellipse, a rectangle or a regular polygon comes out
as that live shape. A shape is only made when every point of the traced outline
lies within 0.5 px (0.2% of the shape's size, if more) of it; an ellipse whose
radii differ by less than twice that is a circle of its area.
The outline before fitting: a corner's own pixel no longer pulls the two edges
that meet there, and a one-pixel step between two long edges is the corner where
they meet. Sharp corners kept: Default 69% -> 76%, the colour presets 86% -> 91%.
The harness gains plain-shape inputs and "shapes %".