These commands edited the document even when nothing changed, so the
call recorded an undo step, emptied Redo and marked the document
modified:
- Fill, Stroke, None, Apply Last Color and Apply Last Gradient with the
paint the objects have already, also on characters selected with the
Type tool.
- stroke.set and stroke.setAdvanced with the options the strokes have
already.
- paint.editGradient, paint.setGradientGeom and the freeform point
commands when the gradient comes out as it was.
- Invert and Complement on the Appearance panel's selected row when no
color changes.
- Swap Fill and Stroke and Default Fill and Stroke when every object
comes out as it was.
- appearance.setItem with the values the item has or an index no
object has, and transparency.set with an item and the values that
item has; appearance.removeItem with indices past every stack;
appearance.moveItem, Reduce to Basic Appearance, Clear Appearance,
the Eyedropper (appearance.copyFrom) and appearance.transfer when
every object comes out as it was.
These commands now return without editing the document. The replies
have the same form as before. On a graph's series or labels, a fill or
stroke that the art has already still records one undo step when the
graph does not keep that paint for them (for example after the
Eyedropper painted the art), and the graph then keeps it.
The Eyedropper test whose second copy changed nothing gives its target
a green fill first, which the copy turns red again.
One line in the evidence list of docs/target-app-parity.md names these
commands, apart from Invert and Complement.
The operation budget counted each operator once, however long a path
it made or read. arc and arct with a huge radius make up to about a
million curve segments, strokepath makes a piece for each dash, and
rectfill makes five path elements for each rectangle in an array.
pathbbox, clip, clippath, shfill, pathforall, gstate, currentgstate
and setgstate read or copy the whole current path or clip path, and
in an encoded user path a byte of the operator string can repeat the
next operator up to 223 times. In a loop, each pass counted a few
operations. vectorcraft-cli info was still reading each of 15 files
that run one of these in "loop" when "timeout 300" stopped it, after
28 to 128 seconds of user CPU time. With a radius of 50 instead of
2.9e30, the loop of arc ran out of the budget after 6.9 seconds of
user CPU time.
Each of these operators now counts the path elements it makes, reads
or copies as one operation each. Making or reading an element took up
to about as long as an operation in a loop of pop, and copying one
took less. strokepath also counts each entry of the dash pattern in
each period of the path. An encoded user path counts each entry of its
data array, each byte of its operator string and each operator it
runs. The 15 loops now end with "the program runs too long" after 0.06
to 2.35 seconds of user CPU time, and the loop of arc with a radius of
50 ends after 1.9 seconds of user CPU time.
Tests: a loop of each of these operators runs out of the operation
budget. The loops cover arc and arct with a huge radius, strokepath
with a fine dash and with dashes of length zero, rectfill with 2,000
rectangles, the eight operators that read or copy a path of 10,001
elements, and uappend on three encoded user paths. The tests read each
program on a thread of its own and fail when no result arrives within
20 seconds.
A shading's Function can be an array of functions. eval evaluated every
entry of an array and the arrays inside it, with only a depth limit. An
array of 100 entries that all refer to the same array one level down,
nested that way a few levels deep, is small in memory, but each
evaluation visited about 100^levels functions, and a gradient samples
such a function 33 times. vectorcraft-cli info on a file with four
levels of 100 shared entries had used 90 CPU seconds, with a 12 MiB
resident set, when "timeout 120" stopped it. eval also counted nothing
against the operation budget: a loop that paints an axial or radial
shading counted a few operations a pass, however many functions each
pass evaluated. On a file that paints a gradient of one Type 2 function
with N 2 in "{ sh shfill } loop", vectorcraft-cli info ran for
209 CPU seconds, and its resident set reached 4 GB before the program
ran out of the operation budget.
eval now counts the functions one evaluation visits, as linear_points
counts its nodes, and raises limitcheck past 257. The PostScript
Language Reference defines a shading's function as one function
dictionary or an array of one for each color component. The reader
allows 32 components, and the depth limit allows each entry a chain of
8 functions. An array with more entries than the visits left raises
limitcheck before it is copied. Each function visited counts 8
operations against the budget: one visit took as long as 6 to 10
operations, measured with loops of pop and of a name lookup.
Tests: a shading over four levels of 100 shared entries raises
limitcheck, and the square drawn before it stays; a shading over one
array of 256 entries is drawn as a gradient, and one over 257 entries
raises limitcheck; a program that paints 2000 shadings over the
256-entry array runs out of the operation budget. The test reads each
program but the last on a thread of its own and fails when no result
arrives within 20 seconds.
Select All, All on Active Artboard and Inverse select from selectable:
the visible, unlocked objects on visible, unlocked, non-template
layers and sublayers, or in isolation mode the visible, unlocked
objects in the isolated group or layer and in its visible, unlocked,
non-template sublayers. Other Select menu commands did not use it:
- Clipping Masks, All Text Objects, Stray Points and Open Paths
selected hidden and locked objects and the objects in hidden or
locked groups and sublayers.
- Point Text Objects, Area Text Objects, Brush Strokes, Bristle Brush
Strokes and the Same commands for symbols, fonts and text colors
checked an object's own flags and its layer's, but not those of the
groups and sublayers between them.
- Next Object Above and Below selected the object beside the selected
one even when it was hidden, locked or a sublayer.
- A saved selection picked from the Select menu selected again the
objects hidden or locked since it was saved.
- Direction Handles direct-selected the hidden and locked paths in a
selected group.
- In isolation mode, the commands in the first two bullets, the other
Same commands, All on Same Layers and a recalled saved selection
selected art outside the isolated group or layer. Next Object Above
and Below and Direction Handles did so only when the selection
already held something that was not inside the isolated group or
layer.
In isolation mode selectable checked the flags of the isolated group's
or layer's objects and sublayers, but not those of the group or layer
itself or of the groups and layers it is in. Select All, All on Active
Artboard and Inverse still selected the isolated group's or layer's
objects after it, or a group or layer it is in, was hidden or locked.
In isolation mode selectable now returns nothing while the isolated
group or layer, or a group or layer it is in, is hidden, locked or a
template layer. walk_selectable visits the objects selectable returns
and the objects inside them, leaving out hidden and locked objects
with what they hold. It visits nothing in a hidden, locked or template
layer, sublayer or group. The commands in the bullets above now select
from it. All on Same Layers takes the objects on the selected objects'
layers that selectable returns. Next Object Above and Below pass over
the objects walk_selectable leaves out, and leave the selection as it
is when none is left in that direction.
The params texts of Direction Handles and Recall Selection,
docs/mcp.md (Object selection, Saved selections) and one line in the
evidence list of docs/target-app-parity.md describe the change.
Overnight doc conflicts were resolved by keeping both sides, which left two or three copies of
some milestone and parity rows (M1–M3, M5–M7, M14; six area rows; one type row). Each is now one
row with every note. The 2026-10-12 progress row takes the 30 PRs landed since and the Film
Grain row that #1282 added separately.
A subpath that geometrically closes (e.g. an ellipse outline) was always emitted
with SPLINE group 70 = 8 (planar only). Autodesk's DXF reference defines bit 0
of group 70 as 'closed', and CAM software such as VCarve relies on it to treat
the cut as a pocket or profile rather than asking the operator to join endpoints.
Now `self.e.int(70, if sub.closed { 8 | 1 } else { 8 })`.
Test: extend lines_are_lwpolylines_curves_splines_and_fills_hatches to assert
the existing closed ellipse's SPLINE group 70 is '9' (1 closed + 8 planar).
These commands edited the document even when nothing changed, so the
call recorded an undo step, emptied Redo and marked the document
modified:
- Change Case to the case the text has, Smart Punctuation on type with
nothing left to change or on a selection with no type in it, and
Find and Replace when every match is replaced with the text it is.
All three rewrite the text runs through edit_runs, which now edits
only when a run changes.
- Convert To Area Type with no point type among the objects, and
Convert To Point Type with no area type among them.
- Type on a Path Options given the values the type has, without Flip:
OK in the dialog with nothing changed, or a Type on a Path effect
the type has.
- Fill with Placeholder Text on type that holds its placeholder text
already.
- type.insert with an empty text on type that ends with a run of
text.
These commands now return without editing the document. The replies
have the same form as before. A call that changes something still
records one undo step.
One line in the evidence list of docs/target-app-parity.md names every
command above except type.insert.
An image larger than the tracer's 64 megapixels was refused. It is now
decoded and box-filtered down to the largest size of its aspect within
64 megapixels, and the traced paths are mapped back to the image's own
size. Images over 192 megapixels are still refused from their header,
before decoding.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XbTizgRsDpFPjtBgZAvfrZ
moveto, lineto and curveto check that the current path holds fewer
than 1,048,576 elements before they add one, and they stop the reading
with "a path has too many points" when it is full. closepath added its
element without that check. In an encoded user path, a byte of 255
runs the next operator 223 times. vectorcraft-cli info on a 622-byte
file with one uappend that runs closepath 11,150,000 times peaked at a
memory footprint of 601.9 MiB and reported "the file draws nothing".
The string operator makes strings of up to 4 MiB, and one uappend of
an operator string that long would add about 468 million elements of
56 bytes, about 24 GiB.
closepath now makes the same check before it adds its element, in the
closepath operator and in the ClosePath arm of append_user. On the
file above, the reading now stops with "a path has too many points",
and vectorcraft-cli info peaks at 62.0 MiB. A path below the limit
closes as before.
Tests: a moveto followed by 1,100,000 closepaths, and a uappend that
runs closepath 1,059,250 times, are each read between two squares.
Each reading stops with "a path has too many points" and keeps only
the first square.
Each tool switch builds a new tool, and a new Artboard tool started on
Artboard 1 whatever artboard was active. With Artboard 2 active, after
the tool was chosen with Shift+O, its toolbar button, Edit Artboards in
Properties or Document Setup, or the Artboards panel menu's Artboard
Options…, the canvas outlined Artboard 1, and Delete, Cut, Copy, the
Transform panel and Artboard Options acted on Artboard 1. The first
pointer event over the canvas, a hover included, then made Artboard 1
the active artboard for Align to Artboard and Paste in Place too.
VectorcraftApp::select_tool now starts a newly chosen Artboard tool on
the active artboard, as the Artboards panel row's Options button does.
Choosing the tool again while it is in use leaves it as it was, and the
artboards Shift-clicked into its selection stay selected.
The test makes Artboard 2 active, chooses the tool with tool.select,
and checks the tool's active option, Artboard Options, a hover over
Artboard 1 and Delete. It then presses Shift+O with two artboards
selected and checks that both stay selected.
Docs: docs/mcp.md (tool.select), docs/target-app-parity.md.
Add the Hungarian catalog (4,524 rows): every menu string, every `tl!`
literal, the contextual and `@id` rows, the plurals and the status and
error messages, written from the meaning of the English labels in
general Hungarian vector-illustration vocabulary. Conventions are in the
header of `hu.tsv`.
- Register `hu` ("Magyar") in `i18n::LANGUAGES` as a complete language
with `plural_none` (a count takes the singular); every `hu-*` locale
resolves to it.
- Tests: Hungarian joins the menu-label, menu-string, `tl!`-literal and
message coverage checks; `hungarian_reads_as_hungarian` pins the core
vocabulary, the plural, the `file.close` override and a nested
message; ő ű Ő Ű are checked in the bundled UI fonts.
- README, ROADMAP, docs/development.md and docs/localization-parity.md
list Hungarian.
Localize native/web file filters, diagnostic labels, numbered presets
and contributor tooltips with English fallback and Czech plural forms.
Fix gradient adjective agreement and disambiguate Japanese printer marks
in PDF/Print choices and summaries. Keep file extensions and names intact.
Add Czech catalog rows and regression coverage; update localization evidence.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Find in Folder… in Linked File Not Found and in the Links panel menu
looks in a folder the user picks, and in its subfolders, for the files
with the names of missing linked files, in any letter case. In the
Links panel menu it looks for the selected linked files when there are
any. It uses the folder search of the Missing Fonts dialog. Find
Linked Files lists each file it looks for with the files found for it.
A file with the size and contents saved with the link comes first. A
file of the saved size comes next when the link saved no hash or the
file is larger than 1 GB, and any other file of the name comes last.
Within each group, a file named in the same letter case comes first.
Then each file's folder names are compared with the saved path's, from
the innermost outward, up to the first one that differs, and a file
with more matching names comes before one with fewer. A file is chosen
when it is the only file found, or the only one with the saved
contents, and the user can choose another file or Don't Relink.
Relink relinks each file chosen, and each relink is one undo step.
With Copy into the document's folder, which shows for a saved
document, each image's file is copied next to the document first. The
file's name, then Name 2.png, Name 3.png and so on, are tried in turn
until one is free, which gets the copy, or is a file with the same
contents, which is used. A placed document is relinked where its file
is. The status line names the first file that fails to relink, and the
other files still relink. When Find Linked Files opens from Linked
File Not Found, the missing files left without a choice, and any that
fail to relink, go back to that dialog. On the web, Find in Folder… is
disabled in the Links panel menu, and links.findFiles can't start a
search.
- links.findFiles searches on separate threads and returns at once;
links.findFiles {} reports the search, and {stop: true} stops it. It
looks for at most 1,000 linked files at once: the active document's
missing ones, or those of the objects it is given. It hashes a file
only when a link of its name saved the file's size and a hash, and
only when the file is 1 GB or smaller. It ends once every linked
file has its same file, and it searches only the folders
text.findFontFiles may search, with the same maxSeconds, depth and
count limits.
- links.relink {copy} copies the file it is given into the saved
document's folder and links images to the copy; a call that includes
a placed document fails. Each name it writes is checked against the
automation write root. text.addFontFiles and links.relink {copy}
make their copies with fileio::copy_new, which never replaces a
file. The text.addFontFiles params text and docs/mcp.md say which
names are tried for a copy.
- vectorcraft-doc adds links::hash_reader, which hashes what a reader
reads as hash_bytes hashes bytes.
- ui.findLinksInFolder is the Links panel's Find in Folder… for agents;
docs/control-protocol.md lists the dialog's fields.
Docs: docs/mcp.md, docs/control-protocol.md, docs/target-app-parity.md.
A click on a Layers row runs layer.setCurrent, which makes the row's
layer current whether it is hidden or locked and keeps the selection.
Object › Arrange › Send to Current Layer then moved the selected
objects into that layer, or into a sublayer of a hidden or locked
layer, and left them selected. In that case drawing, typing and
pasting put new art on the topmost layer that is shown and unlocked,
and since #1039 they fail with "the target layer is locked or hidden"
when no layer is shown and unlocked.
Send to Current Layer now fails with that error when the current layer
is hidden or locked, or is inside a hidden or locked layer. It makes
the check that DocState::target_parent makes for new art
(Document::is_editable) before it moves anything or records an undo
step. The objects stay where they are and stay selected. The message
already has a row in the cs, de, es, fr, it, ja, pt-br, ru and uk
catalogs.
Docs: docs/target-app-parity.md.
Transform Again kept only the last transform's matrix and its Copy
flag. After a Selection tool handle drag resized area type, Transform
Again scaled the type instead of resizing the type area again. After
Move, Rotate, Scale, Reflect or Shear with Transform Patterns set
differently from General › Transform Pattern Tiles, Transform Again
followed the preference, and after a scale whose strokes or corners
param differed from Scale Strokes & Effects or Scale Corners, it
followed those preferences. A recorded Transform Again played back
with the preferences in effect at playback, although its step records
the strokes, corners and patterns values it used.
last_transform now keeps the options with the matrix, as
last_perspective keeps the perspective.transform params.
apply_transform stores the copy, typeAreas and patterns values a
transform command used, and strokes and corners when it scales.
commit_interaction stores the params of a drag's last object.transform
preview with the values that preview noted. object.transformAgain
passes the options to the transform it repeats, and a true or false
value given in its own params under one of those names takes
precedence. A recorded step plays back with the values it records.
Three engine tests fail without the change: Transform Again after a
handle drag on area type, Transform Again after a Move with patterns:
false while Transform Pattern Tiles is on and after a Scale with
strokes: true while Scale Strokes & Effects is off, and a recorded
Transform Again played back with Transform Pattern Tiles on.
Docs: docs/mcp.md, docs/ui-parity.md, docs/target-app-parity.md.
Preferences › Guides & Grid › Gridline every and Subdivisions set the
grid of the documents that were open when either value changed, and of
no other document. A document made with File › New, or opened from a
file VectorCraft reads no grid from (SVG, PDF, EPS, .ai and the other
imported formats), got a 72 pt grid with 8 subdivisions, whatever the
preferences were set to. After a restart the saved values applied to no
document, because the desktop app restores the preferences at launch,
before any document is open.
file.new now gives a new document the preferences' grid, and
document.open, file.newFromTemplate and file.revert give it to a
document read from such a file. A native file, and the native document
that an SVG, PDF, .ai or EPS file carries, keep the grid they were
saved with. Changing either preference still sets the grid of every
open document.
Docs: docs/mcp.md (gridlineEvery, gridSubdivisions),
docs/target-app-parity.md.