VectorCraft listed a variable font's named instances as styles, but
type could not be set between or past them. The reference app's
Character panel has a Variable Font control that adjusts a variable
font's axes, such as Weight, Width and Slant.
Type now keeps Variable Font settings by axis tag
(CharStyle::font_axes, saved as fontAxes only when set). text.setFormat
and text.setRangeStyle take fontAxes: each value is clamped to its
axis's range, a tag the font has no axis for is left out, and null
goes back to the style's own setting. Another font or style clears
them, including one from a character style that does not set them. A
character style captured from type sets them, with {} for none.
text.fontList {family, style} lists a font's axes.
Layout, outlines, the substituted-glyph highlight and PDF export use
the font instanced at the settings, and neighboring runs at different
settings are shaped apart, as runs in different fonts are. SVG export
writes them as font-variation-settings, the wght setting as the weight
and the wdth setting as font-stretch, and embeds the font instanced
there; SVG import reads font-variation-settings back. Legacy .ai export
warns that the settings are left out.
The Character panel shows a Variable Font button for a variable font.
It opens a slider and a field for each axis the font does not hide,
labeled with the font's name for the axis, and screen readers get each
slider under that name. A drag is one undo step. Reset Panel leaves
the settings, as it leaves the font and style.
Missing Glyph Protection now keeps the settings and the font version
on the characters that keep their font; before, it kept only the
family and style. A character style applied or redefined with another
font now clears the old font's version, as text.setStyle does, unless
the style sets one. SVG export with embedded fonts warns when faces of
one weight, style and width share characters, which browsers then draw
in one of the faces.
Docs: docs/mcp.md, docs/target-app-parity.md, docs/type-parity.md.
The Automatic palette counts flat art's colours by area (a colour needs
about 1% of the picture, 0.5% at Colors 100) and fits that count with
median cut and area-weighted k-means. A stem, a highlight, or a near
white beside a pale background (within 24 levels, so in one box) was
averaged into a large neighbour; mixed-colour merging and Noise could
then remove what was left.
On the flat route only, after the count is fitted, rare_colours finds
the colours too rare or too close for it and keeps those that are
distinct (at least 2.5 in Oklab x 100 from every palette colour and from
the main colour each stands for), flat (mostly one histogram bin) and
drawn (a 4-connected region of their own unblended pixels of at least 8
pixels, or three of at least 4); a colour lying between two others, as
anti-aliasing mixes them, must also have an inside. At most six, largest
region first. They stay fixed while the other colours are refitted
around them; merge_mixed leaves them alone; denoise_keeping keeps their
regions of 4 pixels or more whatever Noise is (smaller specks still go).
Full Tone, Limited and Automatic's continuous-tone route are unchanged,
and denoise without kept labels is as before.
Methods cited in the code: median cut (Heckbert 1982), k-means (Lloyd
1982), Oklab (Ottosson 2020), salient-colour preservation (Fukuda et al.
2019, doi:10.1117/12.2521607). The rule and its numbers are VectorCraft's
own; Adobe does not document how its palettes are chosen.
Tests (code-drawn): a rare stem or patch over 2-8 flat colours, hard
and anti-aliased at 1x and 2x; near-white highlights on #FFF4E0 kept and
still paths at Noise 16; a 3 px #909090 stem on #808080; scattered
single pixels of the same areas add no colour; denoise_keeping at Noise
0-32.
Measured on 66 synthetic flat logos (22 designs x hard 1x, anti-aliased
1x and 2x; local inputs): every key colour and no spurious one in 66
(was 39); missing key colours 0 (was 43), spurious 0 (was 2), merged
pairs 0 (was 46); rendered dE00 0.348 -> 0.138. Four public-domain scan
crops take the continuous-tone route and are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tBcdwNhptqtmX32JCGFzB
The public documentation says Image Trace warns about images larger
than 1500 pixels on a side but traces them, only more slowly. VectorCraft
traced them silently, and refused any trace of more than 2,000,000
anchors, which a 4000 x 3000 photo at High Fidelity Photo nearly reaches
(1.9 million on a code-drawn photo) and a noisier one passes.
- imageTrace.make and makeAndExpand return `warnings`: one for an image
over 1500 px on a side (traced, but more slowly; a lower-fidelity
preset or a smaller copy is quicker), one for an image over 64 MP
that was traced from a scaled-down copy. The app shows the first in
the status bar.
- MAX_TRACE_ANCHORS is 8,000,000 natively (2,000,000 on wasm, where a
page has at most 4 GB). Measured through imageTrace.make on
code-drawn photos: about 90 bytes an anchor kept in the document;
7.9 million anchors kept 685 MB with an 876 MB peak (4000 x 3000,
Full Tone at Colors 100, Paths 100, Noise 1, 37 s).
- The refusal keeps its translated advice: raise Noise, lower Paths or
Colors, or pick a lower-fidelity preset.
Tests: a_large_image_traces_with_a_warning (1600 x 40 and 1500 x 8
code-drawn images, make and make-and-expand),
trace_warnings_name_a_large_or_scaled_down_image, a compile-time check
of the limit's range, and a UI test that the status bar shows the note.
Source:
https://helpx.adobe.com/illustrator/using/image-trace-results-optimization.html
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tBcdwNhptqtmX32JCGFzB
Raster::new checked its RGBA length with assert_eq!, so bytes of the
wrong length (from a file, a plug-in or a control message) would abort
instead of reporting an error. It now returns Result<Raster, TraceError>
and gives TraceError::Decode naming the byte count and the size (the
existing variant, whose text is already translated, instead of a new
message every language would need). The size product is checked for
overflow.
Raster::pixel indexed the bytes directly; it now gives transparent
black outside the raster, or past the end of a raster whose public
fields were set to a size its bytes don't have (which mosaic reads).
The public signature changes: every caller in the workspace was a test
(trace unit tests, Flat Logo tests and the quality harness), updated to
unwrap. No shipped code built a Raster with new; decode, decode_to_fit
and from_fn always make the right length, and trace_within already
refuses a raster whose fields disagree.
Test (code-drawn): a_raster_of_the_wrong_length_is_an_error_not_a_crash
(short, long, empty, zero-width and overflowing sizes; pixel, trace and
mosaic on a raster whose fields disagree).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tBcdwNhptqtmX32JCGFzB
Place junction clusters at their deepest pixel and prune terminal branches
by significance beyond their maximal discs. Merge overlapping junctions
and split crossings with continuing outer arms, then join degree-two runs.
Bound nodes, branches and simplification work, using checked data access.
Keep whole-blob selection, ink refinement, cap detection and fitting rules.
Test junction degrees, crossings, 24-angle lines, pruning, graph caps and
random-blob determinism. Record improved junction degrees and remaining
spur and extra-junction limitations in the centre-lines measurements.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: OpenAI <noreply@openai.com>
Compute exact squared Euclidean distances per blob with separable parabola
lower envelopes. Thin with bounded Guo–Hall active sets and retain diagonal
ends during staircase cleanup. Keep the whole-blob width rule and existing
graph, ink, cap and fitting rules, with checked access in the moved module.
Verify EDT against brute force, active sets against plain rounds, and both
diagonal slopes for length and connectivity. Record the centre-lines rerun
and its remaining limitations without changing feature scores.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: OpenAI <noreply@openai.com>
Measure Create Strokes on exact code-drawn scenes in aa and hard variants,
including centre/width error, topology, ends, spurs and painted coverage.
Keep the existing allocator report in one ignored test and document the
current baseline without changing tracing behaviour or dependencies.
Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each swatch or graphic style library now opens in a panel of its own,
`library:<kind>:<library id>`, in the shared docking system: several can be open
at once, and they float, group as tabs, dock and close like any panel.
Opening a library that is open brings its panel forward; Previous / Next
Library show the neighbouring library in the same panel.
The library panel menu gains Persistent. The panels' view, Show Find Field
and Persistent are kept in the UI preferences (`library_panels`); at startup
the persistent panels reopen where the saved layout had them, a library
loaded from a file read again from its path (a missing file is skipped with
a status-bar note). `ui.libraryPanel {panel, library?, view?, showFind?,
persistent?}` reaches all of it; docs/mcp.md documents the panels.
Install Plug-in (plugin.install) now keeps the plug-in's module in a
Plug-ins folder next to the preferences, as Illustrator keeps plug-ins in
its Plug-ins folder, and the desktop app and vectorcraft-cli install the
ones there at startup (a module that doesn't load is skipped with a log
record, at most 512 MB are read). Remove Plug-in deletes it from the
folder. The Additional Plug-ins Folder preference is still loaded from
where it is. Confined MCP servers and VECTORCRAFT_NO_PREFS keep nothing,
as with the PDF preset and colour profile stores.
Docs: docs/mcp.md, docs/plugins.md, docs/target-app-parity.md.
The History panel looked up each step's name with tl!, which reads only
a plain catalog row. 95 of the 427 names that the engine, the tools and
the shell give their undo steps had no such row in at least one complete
catalog, and 83 had no row at all (Make Blend, New Guide, Pen, Reshape
Spine and others). A step named after an effect has the effect's menu
name without its ellipsis. For many effects, and for steps such as New
Layer, the catalogs hold that name only with the ellipsis. The panel
listed those steps in English.
Each complete catalog now has a row for every step name, written in the
catalog's own terms: 1,437 rows in the 19 catalogs. The History panel
reads a step's row, or else the row of the same name with an ellipsis,
without the ellipsis, and shows the name as recorded when the catalog
has neither. A new test reads the step names from the sources, as the
tl! and message tests read theirs, and requires a row for each in every
complete catalog.
Docs: docs/target-app-parity.md.
Selecting other art drops Puppet Warp pins, ending Edit Text clears the
flat view, and a font scan lays type out again. None of these makes an
undo step or counts as an edit to save, but each one replaced the
document. A background save that finished afterwards compared the
document with the one it wrote by pointer, so the document stayed
marked modified. Data Recovery also wrote a new copy of a document whose
pins had been dropped.
DocState now records the documents that these changes replaced, and
DocState::same_content counts them as having the same content. The
modified flag, the end of a background save and Data Recovery use it.
After Edit Text ends, an undo state that was the saved one stays the
saved one.
With the Artboard tool in use, a Shift-click on a row of the Artboards
panel selects the artboards from the last row clicked without Shift to
the clicked row, and the clicked row's artboard becomes the active one.
When the artboard of the last row clicked without Shift is no longer
selected, the range starts from the active artboard, and later
Shift-clicks start from there too. Ctrl/Cmd-click still adds or removes
one artboard, and a plain click selects one. The public Select
artboards page lists Shift-click in the Artboards panel for selecting
several artboards.
A Shift-click used to add or remove one artboard, as Ctrl/Cmd-click
does (#1139).
Docs: docs/target-app-parity.md.
Print and Print Preview name the area type and type on a path that
prints without the text past the end of its frame (#2308). The exports
that draw type gave no such warning: a PDF, EPS, SVG, EMF, WMF or
raster export of a frame too small for its text answered with no
warning about it.
These exports now add the same warning, with "isn't drawn in the
export" in place of "doesn't print". Each export counts the visible
objects that it draws. Those are the objects on the pages or artboards
it writes, or all of them in an EPS of the visible art and in an SVG
without chosen artboards. A PDF leaves out layers that don't print
unless includeNonPrinting or createLayers is set. The objects are found
with the walk that fonts::printed_type runs for Print, and typecmd.rs
builds the message with the code that builds Print's. A .ai file keeps
the whole text in its editing data and gives no warning.
Docs: docs/mcp.md (Fitting area type), docs/target-app-parity.md.
With View > Guides > Lock Guides on, Session::select drops the paths
made into guides from the selection (#1534), but commands that set the
selection another way skipped that filter and left a locked guide
selected, so the next move moved it. object.exitIsolation selected an
isolated guide path. Undo, Redo, a canceled undo group, a canceled
drag or dialog preview and a failed command.batch brought back locked
guide paths and ruler guides in the selection. Unlock All, Show All
and Paste selected the guide paths they unlocked, showed or pasted.
Locking the guides deselected them in the active document only.
The filter is now deselect_guides, which also clears the selected
ruler guides. Session::select and Session::edit run it while Lock
Guides is on, and so do object.exitIsolation, Undo, Redo,
Session::cancel_interaction, a canceled undo group and a failed batch.
Locking the guides deselects them in every open document. With Lock
Guides off, the selection is set as before.
Docs: docs/mcp.md.
A marquee measured a clip group, and the objects on a layer with a
clipping mask, by all of their art, including the art the mask hides.
A marquee around a rectangle drawn next to a clip group also selected
the group when it crossed the group's hidden art. A click already
counts only the art inside the clipping path.
The marquee now takes a clip group, or an object on a clipping layer
or sublayer, only when it meets the area inside the clipping path, and
it measures the art there within that area's bounds. The same rule
applies to the objects that Direct and Group Selection select whole,
to Group Selection's E marquee, and in isolation mode when the clip
group or clipping layer itself, or one object directly inside it, is
isolated. The anchor points that Direct Selection's marquee picks are
unchanged.
Docs: docs/target-app-parity.md.
Nothing locked the variables: any command or panel button could delete
or rename a variable that scripts and data sets name.
variable.lock {locked?} (Lock Variables: a lock button in the Variables
panel's bottom bar and an item in its menu) sets a flag that is saved
with the document. Without `locked` it toggles, and a change is one
undo step. While the flag is set, variable.define, variable.delete,
variable.rename and the four variable.make…Dynamic commands are refused
with "variables are locked", and the panel disables New Variable…,
Variable Options…, Delete Variable and Make … Dynamic. Binding,
unbinding and the dataset commands still run. variable.list
reports `locked`. A file without the flag opens unlocked.
Docs: docs/mcp.md, docs/target-app-parity.md.
Document Info's Save, Save Swatch Library and Save Graphic Style
Library to a file, the Libraries panel's Export Library…, the presets
dialogs' Export…, the CSS Properties export, Print to a PostScript
file and Keyboard Shortcuts › Export… passed no file type to the save
panel. On Windows the panel listed "All Files", and neither the Windows
nor the macOS panel added the extension to a name typed without one.
Each of these saves now passes the one file type it writes, as Save and
Export do for document formats. The Windows panel then lists it, and
the Windows and macOS panels add its extension. The labels are the ones
the open dialogs use for these files (Swatch libraries, Libraries, the
presets, Shortcut sets), the Window menu's Graphic Style Libraries, Text
for Document Info, CSS and PostScript.
A selection with more than 100,000 anchors shows its bounds instead of
every outline and anchor (#525). The count took in path anchors only.
Type, images, symbol instances, placed documents, gradient meshes and
envelopes counted nothing, although the selection overlay outlines each
of them: a box, an area type frame or a type path, a baseline marker
for point type, and the points and mesh lines of meshes and envelopes.
The PDF importer makes type objects from a page's text lines, one image
object per image and one gradient mesh per patch or triangle of a mesh
shading, so selecting all of a large PDF outlined each of them: 131,072
selected mesh pieces need 16.8 million vertices, a 336 MB vertex
buffer, past wgpu's 256 MiB buffer limit.
The count now takes in what the overlay draws for each kind. Paths
count their anchors. Area type and type on a path count the anchors of
their frame or path, and point type counts six: its box's corners and
the ends of its baseline marker. Images, symbol instances, placed
documents, and area type or type on a path in perspective count the
four corners of their box, and gradient meshes and envelopes count
their points and the ends of their mesh lines. The selection's outline,
the key object's outline and the hover highlight all use the count.
Docs: docs/target-app-parity.md.
A click with the Group Selection tool on a compound path selects the path
under the pointer (its outline within the pick tolerance, else the innermost
path whose area holds the click), so the outer shape and each hole can be
selected on their own; Shift-click adds another, and a click on a selected
path adds the compound path, as a click on a group member climbs to its group.
A path selected this way is an object of its own for Move, Transform, Rotate
and the other transforms, as it already was inside an isolated compound path:
the hole of a ring moves without the ring. Paint and the other commands still
go to the compound path, which holds the paint.
Docs: docs/target-app-parity.md, docs/ui-parity.md.
# Conflicts:
# docs/target-app-parity.md
On an edge two artboards share, the artboard to the right of it or below
it holds a point, except at the end of the edge that lies on that
artboard's bottom or right edge, such as where both artboards' bottom
edges meet. There the first artboard holds it, or the last when
topmost. The doc comment and the test comments now say so, and the test
asserts that corner.
The Print Tiling tool's press and print.tiling.set without `artboard`
decided which artboard a point is on with Rect::contains, which leaves
out the right and bottom edges. A press exactly on an artboard's right
or bottom edge picked no artboard when no other artboard holds that
point, so the drag did not snap to that artboard's edges. An origin
there was measured from the first printed artboard: with artboards at
0 to 300 pt and 300 to 600 pt, an origin on the second one's right edge
was saved as x 600 instead of 300.
Both now count the edges as Document::art_on_artboard does, within a
millionth of a point.