Each pointer move of a drag applies the move to the interaction's
snapshot, so the dragged object is a new node in every frame. The
renderer kept the rasters of an object's drop shadows, outer glows and
filtered content (Gaussian Blur, Feather and the Photoshop-style
filters) only for the same node, so every frame of a drag ran those
effects again. On main, a moved frame of texture_drag_perf (a CMYK
rounded rectangle at 190.07% and DPI 1.5 in an 1800 x 1500 view) took
112 to 197 ms in a release build.
The canvas now renders with RenderOptions::interacting while a drag or
a dialog's preview is in progress. When an object misses its cached
raster in such a frame, the renderer takes the raster an earlier frame
made for an object with the same id, slot and ink at the same zoom and
rotation, made by the same effects from content of the same size (to
within 0.000001 pt). It draws the object's content into a raster
placed as that one was and compares the pixels byte for byte (only the
alpha under a shadow or glow). For Feather it also compares the points
of the clip outline, to within 0.000001 pt. When they match, the old
raster is drawn at the new place, rounded to a whole pixel, as a pan
draws a cached raster. Content under De-Interlace never takes a moved
raster, and a raster made over moved rasters counts as moved itself.
A frame rendered without interacting never draws such a raster, or a
raster made over one: the frame after the drag runs the effects again
and draws what a fresh renderer draws. The canvas's cache key includes
whether an interaction is in progress. On the desktop, the canvas
sends the frame after an interaction to the render worker when the
frame shown drew moved rasters. Each cached raster keeps the content it
was made from, within the existing 512 MiB budget. The pixels kept
with a filtered raster take as many bytes as the raster, and the alpha
kept with a shadow or glow takes a quarter as many as the shadow.
texture_drag_perf now renders a drag's frames as the canvas does, then
the frame after the drag, and that frame once more with every raster
cached. A moved frame now takes 20 to 26 ms, close to the 18 to 25 ms
of the cached frame; the frame after the drag takes 110 to 189 ms.
Docs: docs/target-app-parity.md.
Area type whose frame is too short for a line of the font it shows in here
drew nothing: a font that isn't available shows in the fallback font, whose
line may be taller than a frame made for one line of the file's font (overset
type isn't drawn). Where the file itself lays lines out in the frame, the
frame of the imported text object is made taller, from its top, by what its
first line needs, and the open result says how many frames were.
A dialog opened by a click is drawn in the frame of that click, and in that
frame egui takes the focus from any field the click missed. The first time a
dialog's window shows, that frame only measures it, so its first field took
the focus a frame later and kept it; every later time the field took it in
the click's frame and lost it at once. The field now takes it the frame after
the click.
A .ai file with placed files opened from its PDF part (no guides, no sublayers,
no hidden objects), because the editing-data reader stopped at them.
- Lexer: a placed file's preview section is one token (it was skipped).
- Reader: %AI5_BeginPlace ... ~ becomes a linked image with the file's path,
showing the preview the data keeps of it (premultiplied ARGB, capped at
MAX_PIXELS, its rows checked against the bytes there), in its place: the
matrix takes the file's pixels from the canvas, where they start at the point
of art space's origin. A placed file without a preview, a path or a matrix
still makes the file open from its PDF part.
- The open result lists one warning saying linked pictures show the
low-resolution preview until their files are found (rows in the catalogs).
- Engine: a link nothing is known of whose image shows a preview is read where
it is found as the document opens, into the image's bounds, and isn't
reported as modified.
- Tests: lexer, reader (hand-written data), engine open with the link missing
and found, fuzz tokens.
- Docs: file-format-parity, target-app-parity, gaps, ROADMAP.
Its View row asked for the row's whole width for its menu before its label was laid out, so
the row ran past the panel, the panel's content grew and the start of every label was cut off.
The buttons were placed (WIDTH - 36) / 2 from the panel's content edge, but
the docked panel's content is narrower than WIDTH: the 1.5 pt frame stroke on
both sides and the 1 pt separator line egui reserves on the right leave 44 pt.
So the single column sat 6.5 pt from the left border and 1.5 pt from the right
(2.5 pt right of centre), and the double column's second button ran over the
right border. Centre each row on the panel's inside instead, and widen the
double column to 80 pt so its two buttons fit with 2 pt either side.
A click with the Polar Grid tool opens the shared shape dialog (width, height, concentric and
radial dividers), as the Rectangular Grid tool does, and keeps the divider counts as the tool's
options.
Object › Align ran object.align without to, so it aligned to the key
object while there was one and otherwise to the selection. With Align
to Artboard chosen it aligned two objects to the selection's bounds,
and it left a single selected object where it was, while the align
buttons of the Align panel, the Control bar and the Properties panel
move that object to the active artboard. The Align To choice was kept
in egui's temporary memory, so it went back to Align to Selection at
each launch, and ui.inspect did not report it.
The choice is now the UiState field align_to, which the desktop app
saves with its preferences and ui.inspect reports. A saved value other
than "artboard" reads as Selection, and the other preferences still
load. Object › Align now sends the to that the align buttons send: the
artboard when one object is selected, else the key object while there
is one, else the Align To choice. With anchor points selected it sends
its parameters as before, and the points align to their own bounds.
While one object is selected, the Align To row shows Artboard. Agents'
object.align calls without to keep the engine's default.
Docs: docs/control-protocol.md, docs/target-app-parity.md.
Preferences › Appearance of Black › On Screen was stored and never read.
Display All Blacks Accurately (the default) shows pure CMYK black, black
ink alone at 100%, through the working CMYK profile, a dark grey, as the
canvas always did. Display All Blacks as Rich Black now shows it on the
canvas as RGB black: fills, strokes, type, gradient stops, effect colours
and pattern tiles, and in CMYK documents drawn by ink planes a pixel
whose inks come out as black ink alone. Rich blacks, black tints and
placed images show the same either way; exports and printing are
unchanged (Printing / Exporting is a separate option).
The renderer gets RenderOptions::rich_black, painted with a new
Ink::RichBlack (screen colours with pure black as RGB black) and applied
by ink::compose. The canvas sets it from blackOnScreen and keeps it in
its cache key, so changing the preference redraws.
A headless frame test reads the canvas raster: K100 shows its profile
grey by default, RGB black with rich, a rich black unchanged, and grey
again with accurate. A render test covers the opaque and ink-plane paths
and checks that rich blacks, tints and images don't change.
Docs: docs/mcp.md (blackOnScreen), docs/target-app-parity.md,
docs/gaps.md, docs/ui-parity.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add a Korean catalog (crates/ui-egui/src/i18n/ko.tsv, 4,151 rows) covering
every menu string, every tl! literal and the status and error messages, and
register `ko` in i18n::LANGUAGES (plural_none; every ko-* locale resolves).
- tests: korean_reads_as_korean, ko locale tags, ko in KEEPS_MENU_NAMES and
COMPLETE_MESSAGES; is_cjk covers Hangul so the bundled-font glyph check
leaves Hangul to installed fonts, as it does CJK without craft-fonts.
- Hangul is drawn with an installed system font on the desktop; the web
build still needs a Korean UI face in craft-fonts.
- docs: localization-parity (Korean row, 12-language score 54% -> 63%,
60-100 h), ROADMAP, README, development.md, gaps.md G16,
target-app-parity.
Clean-room: written from the meaning of the English strings in ordinary
Korean vocabulary; no other product's Korean localisation was used.
AI-drafted; native-speaker review welcome.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Save serializes JSON straight into the compressor and writes images from their Arc into the
file; open reads each image into its own buffer, and NodeKind reads its tag first instead of
buffering every node. Already-compressed images are stored rather than deflated again in the
same gzip stream. Limits apply per section, so any file VectorCraft writes opens again. Data
Recovery writes each image once, content-addressed, instead of every interval. Native files open
on a background thread. Files stay readable by main both ways; JSON-only saves are identical.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmMmc1J8XeNLxhRihWv9cc
Rarely set fields move into one optional boxed NodeExtras, and large NodeKind payloads (images,
live shapes, blends, envelopes) are boxed. Every object, and every undo step that copies it,
shrinks: a 50k-path move keeps ~24.5 MB per step instead of ~40 MB. Files stay byte-identical
(Node keeps its flat JSON through a borrowed Serialize and a from-wire Deserialize, no flatten),
and loading is slightly faster. Undo and redo reuse their step's size estimate instead of
walking the document again (8.3 -> 5.6 ms for a 50k-path move).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmMmc1J8XeNLxhRihWv9cc
# Conflicts:
# crates/engine/src/cmd/graph.rs
# crates/svg/src/export.rs
The navigator re-rendered the whole document on the UI thread on every revision, including
each drag step, and layer thumbnails re-rendered whole layers the same way. Both now keep the
last image while an interaction runs, render on a background worker once the document settles
(synchronously on wasm and in tests), and turn on image mips. Thumbnails are keyed by node id
with a Weak, not by a reusable address.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmMmc1J8XeNLxhRihWv9cc
History was limited by step count only; a whole-selection move of 50k paths keeps ~40 MB per
step. Steps now carry a size estimate (only unshared subtrees are visited) and the oldest are
dropped beyond a byte budget, keeping the last 8. Documents entering history drop their id
index, selections are shared between steps, the command journal is bounded (held while an
Action records), and document resources are copy-on-write. Saved files are byte-identical.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmMmc1J8XeNLxhRihWv9cc
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.
Add deterministic document-space grain, highlight controls and a live
dialog with preview, cancel, undo and appearance editing. Preserve
coverage, support existing native and raster export paths, translate
new labels and update parity documentation.
Validate the tonal curve, stationarity, subpixel strength, zoom filtering
and tile seams. Full cargo xtask ci passes all seven gates.
The branch was cut from an older main, so its commit also removed four engine tests (stroke and
paint rejection, malformed anchor selection, bad anchors in path commands), the newer
window.panel.float/dock parameter docs and the un-collapse on showing a floating panel. This puts
them back and adds the Horizontal & Vertical Align Center label to the complete catalogs.
Object › Graph › Data (and the graph tool after drawing) opens the
Graph Data window as a worksheet, as
https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/visualize-data/add-graph-data.html
and
https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/visualize-data/adjust-column-width.html
describe, in place of a free-text CSV field.
- The first row holds the series labels, the first column the category
labels, the other cells the values. A click picks a cell, edited in
the entry box above the sheet: Tab enters it and moves right, Enter
down (Shift: the other way), the arrow keys move between cells.
Pasting tab- or line-separated text fills cells from the picked one.
- Import Data reads a tab-separated text file (UTF-8, or UTF-16 with a
byte-order mark), at most a graph's 10,000 × 256 cells.
- Transpose row/column and Switch x/y (scatter graphs) rearrange the
sheet; Revert reads the graph's data again; Cell Style sets the
decimals shown and the columns' width. Apply redraws the graph and
leaves the window open.
- The sheet reads and writes the graph's series, categories and rows
(not CSV), so labels with commas, quotes or `|`, blank labels and
graphs without category labels come back as they were.
- Only the cells in view are drawn, read in place, so a full-size sheet
stays responsive.
Own choices, as the pages don't say: Number of Decimals only changes
what the sheet shows; Transpose is off when it would make more series
than a graph holds; the arrow keys move between cells rather than the
caret. Dragging a column's edge to resize it isn't supported yet.
Graph Data's Transpose row/column and Switch x/y, as
https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/visualize-data/add-graph-data.html
describes, a `|` in a label as a line break, as
https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/visualize-data/add-graph-labels-and-data-sets.html
describes, and the pie graph options of
https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/visualize-data/format-pie-graphs.html
- graph.setData {transpose: true} swaps rows and columns (after any new
data in the same call); a graph with more categories than a graph can
have series (256) is refused rather than cut. {switchXY: true} swaps
each scatter series' y and x columns, labels included.
- A `|` in a category or series label starts a new line: legend rows
grow with their lines, labels beside an axis or in a wedge centre
theirs. A label of only `|` is blank.
- Pie graphs: pieLegend none|standard|wedges (Legends in Wedges puts the
series labels inside their wedges, in white on dark wedges, drawn over
the pies), piePosition ratio|even|stacked and pieSort all|first|none,
as dropdowns in the Graph Type dialog, which lists them for pie graphs
only.
Own choices, as the page doesn't say: Position and Sort keep today's
drawing by default (Even, None); Ratio sizes pies by area, the biggest
keeping the full size; Stacked pies share a centre, each drawn as the
ring outside the next smaller one so none covers another, with their
names over them.
New fields are written only when set. Graphs with default options draw
the same art as before.
The Graph Design dialog lists the designs with a preview of the
selected one and has New Design, Rename, Delete Design, Paste Design
and Select Unused, as the Graph Design dialog of
https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/visualize-data/reuse-graph-designs.html
works; the Marker dialog lists the designs.
graph.design takes them as params:
- new: name or true (the selected art, named "New Design", numbered
when that's taken);
- rename + to: the graphs drawing the design follow it to its new name;
- delete: a name or a list of names;
- paste: as before;
- no params or selectUnused: the designs, and those no graph draws with.
The engine's drawing is unchanged.
Graph designs are named art a graph draws in place of its default
marks. Marker designs follow
https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/visualize-data/apply-marker-designs-to-graphs.html
and the Graph Design dialog
https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/visualize-data/reuse-graph-designs.html
- graph.design saves the selected art as a named design (Save Design),
pastes a copy of a design into the document, selected, to edit and
save again (Paste Design), and deletes one; graphs drawing a deleted
design go back to their default marks.
- graph.marker draws the data points and legend swatches of the series
picked with Group Selection (or listed, or every series) with a
design: its backmost object is scaled to fill the default marker's
square, strokes, effects and pattern tiles scaling with it. Line,
scatter and radar series; null goes back to the default square.
- Object › Graph › Design… and Marker… open dialogs listing the
document's designs.
Own choices, as the pages don't say: the backmost object's bounds, not
only a rectangle's, set the size (a single object uses its own bounds);
Delete Design manages the list; a design keeps its own paint, so
painting a series doesn't recolour it.
Designs are stored in a new `graphDesigns` list and series designs in
`seriesMarkers`, written only when present. A graph saved as a design is
kept as the plain art it draws. A design holds at most 2,000 objects, a
document 1,000 designs, and a graph whose designs would draw more than
200,000 objects draws default squares; a file's designs are tidied the
same way on open. Global swatch edits, image relinking and placed
documents see design art like symbol art. Graphs without marker designs
draw the same art as before.