Document Info › Objects counts the paths but didn't say how long they are,
which sign and print work reads there (an LED strip along the letters, a
cut line). `document.info` now adds up the length of every path it counts,
the pieces of compound paths included and guides left out, and returns it
as `pathLength` (pt); the Objects section shows it as Path Length in the
document's units, right after the Paths count, so Selection Only gives the
selected paths' length. The label is translated in the catalogs that
cover the menus; pt-br only takes strings the UI itself shows.
Panel (≡) menu rows, and the Control bar's image menu, had no frame at all,
so nothing showed which row the pointer was on. They now draw the hover
frame the menu bar's items draw.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Selecting art scrolls the tiles to its swatch once (centred) and clears the
panel's own selection, so the art's swatch is the one highlighted; so does a
change of the selection's paint (after Recolor Artwork). An unlinked process
colour highlights the swatch of that very colour. A selected image, or text
in a colour no swatch has, highlights nothing (not None).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Control bar's file name is a menu: Replace…, Go To Link, Edit Original,
Update Link and Placement Options…. Replace… (also in the Properties panel,
in place of Relink…) puts the picked file in the image's place fitted inside
the old bounds and centred, keeping the new file's aspect ratio instead of
stretching it into the old one. links.relink takes `preserve` (transforms,
bounds, fileDimensions, fit or fill) to override the Placement Options for
that read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With Limit to Library, a row's new colour is the library swatch itself: art
links to a global or spot colour (a Pantone-style spot colour stays a spot
colour in its own model, tints keep their tint) and a process swatch gives
its colour in its own model, instead of being converted to the model of the
colour it replaces. The swatches used join the document's swatches (one the
document already has, same name and colour, is reused); recolor.apply
reports them as `swatches`. A folder button next to Limit to Library loads a
library file (.ase, .acb, .gpl, .vcswatches or a document's swatches) and
limits to it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A palette button opens Recolor Artwork when the selection has colours to
recolour (solid colours, gradient stops or mesh points, inside groups too):
in the Contextual Task Bar, before Lock, and in the Control bar, after the
Graphic Style picker. Art painted with None shows no button.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Zoom Out and Zoom In sit to the right of the font family and style menus. They step the preview cells through 16, 22, 30, 42, 56, 72 and 96 pt; the grid still opens at 30 pt, and the buttons stop at the ends. The step is stored on the UI state and written with the other panel choices in ui.json when the app quits. Zero is the default and is left out of the file, so older preferences stay as they are.
Abutting traces must draw each edge between two neighbouring regions once:
the filled paths on either side follow one curve, one of them reversed, and
together cover the image without gaps or overlaps. These tests check that
from the trace output alone, with no renderer: points along each path's
outline must lie within 0.001 px of another path's outline, the paths' areas
must sum to the image's, and every point of a 4 x 4 per-pixel grid must lie
in exactly one path.
The images are drawn in code, hard-edged and anti-aliased, and traced in
Color (Abutting) and Black and White, each at the default settings, Paths and
Corners 0 and 100, Snap Curves To Lines, and Shapes. They cover a closed island
in a background, a ring with and without a core of a third colour, a wavy
two-colour seam, a three-region junction, outlines that leave a junction and
return to it (the 111 / 101 / 110 pattern at 1, 2, 3 and 6 px, and framed),
squares touching at a corner, a one-pixel hole and a one-pixel island (kept at
Noise 1, dropped together with what they leave at Noise 25), separate areas of
one colour, and slanted hatches with one-pixel paper channels. A further test
shows the check finds a 0.01 px shift and stacked (Overlapping) layers.
Two tests are ignored as documented current gaps:
- At Paths 0 and Corners 0, a piece returning to its own junction collapses:
its region is dropped and its area left uncovered (draft #2205 fixes it).
- With Shapes, a region whose outline runs through a junction becomes an exact
shape its neighbour does not share (up to 0.0039 px in Black and White).
No tracer code changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tBcdwNhptqtmX32JCGFzB
In Grayscale mode with Gradients on, the trace could fill paths with
coloured linear gradients (for example a blue-to-orange ramp) beside
grey flat fills. Gradient detection ran on the RGB image, so the stops
and the gradient's palette colour (its middle) were the source colours,
while Grayscale's flat fills come from each pixel's luma.
Grayscale now finds gradients, and settles their edges, in a copy of the
image reduced to the same Rec. 601 luma the flat grays are quantised
from. Every channel of that copy is equal, so the fitted stops, the ends
carried to the area's edges and the palette entry are all grey. Color
mode is unchanged: it still detects on the RGB image (trace output for
gradient fixtures is byte-identical before and after).
Adds grayscale_gradients_are_grey: a code-drawn sky ramp over flat
shapes with a ramp rectangle, traced in Grayscale with 4 grays and
Gradients on, must still produce a gradient, and every path colour,
gradient stop and palette entry must be grey.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tBcdwNhptqtmX32JCGFzB
When a long horizontal or vertical run crosses a three-colour junction one
or two pixels before a corner, the corner correction for that run borrowed
a neighbouring crack's shift that the region on the other side of the
junction never applied, leaving hairline gaps between abutting colours.
The correction now applies only when all three cracks it uses lie in the
same shared piece.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tBcdwNhptqtmX32JCGFzB
The 'Last updated · Change' status lines in docs/roadmap.md, docs/target-app-parity.md and docs/image-trace-quality.md change with almost every merge to main, so this PR's edits to them conflicted each time main moved. They are reverted to main's text; this PR's evidence bullets, history rows and table entries stay.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tBcdwNhptqtmX32JCGFzB
The 'Last updated · Change' status lines in docs/roadmap.md, docs/target-app-parity.md and docs/image-trace-quality.md change with almost every merge to main, so this PR's edits to them conflicted each time main moved. They are reverted to main's text; this PR's evidence bullets, history rows and table entries stay.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tBcdwNhptqtmX32JCGFzB
At 180° and 360° of twist, Extrude & Bevel and Inflate drew some wall
strips in the wrong order, and a far strip showed through a near one.
Each strip ran the whole length of an outline edge and was sorted by
the depth of its center, while a twisted wall can cross it far from
that center. The caps were sorted the same way.
Twisted walls and bevels are now drawn in pieces about as long as a
wall slice is wide (fewer near the 32,768-facet limit), and twisted
bevel steps turn at most 6° each, as wall slices do. A cap, or
Inflate's flat back, that faces the viewer is drawn after the walls,
and edge-on pieces of a twisted wall keep their own depth. Pieces that
face the viewer are drawn where their strip as a whole faces away, so
a twisted solid seen from behind shows its walls' corners around the
back cap. Untwisted solids give the same facets as on main.
Over 80 twisted squares, the facet seen over a point lay up to 79.7 pt
behind a nearer facet on main and now lies at most 5.0 pt behind.
"Twisted" in 72 pt type, extruded with a 360° twist, takes 12,311
facets instead of 7,622 and 3.1 ms instead of 1.9 ms.
Docs: docs/three-d.md.
The reference app's Scribble Options have a Variation for each of Path
Overlap, Curviness and Spacing. Scribble had one `variation`, which
moved each line's ends both along the line and across it, and every
turn of a scribble took the same curviness. An angle such as 270 was
stored as given.
Scribble now takes `overlapVariation` (pt, default 0.5), which moves
the ends along the line, `spacingVariation` (pt, 0.5), which moves
them across it, and `curvinessVariation` (%, 0), which varies the
curviness from one turn to the next. The dialog shows each Variation
after the option it varies, and the Appearance panel lists all three.
A Scribble saved or scripted with the earlier `variation` reads it as
the Path Overlap and Spacing variations. With the defaults, and with
the earlier `variation`, Scribble draws the same outline as before.
`effect.apply` and `effect.setParams` store an angle above -180 and up
to 180, so 270 is stored as -90, and the dialog shows the stored value.
The Appearance panel's inline editor lists an effect's values in
their current form, as the dialog does. It listed an earlier key next
to the defaults of the keys that replace it.
Docs: docs/effects-parity.md (Stylize row), docs/target-app-parity.md.
Effect › Blur › Smart Blur had only its Normal mode: the dialog and
blur.smart took Radius, Threshold and Quality and drew the blur.
Smart Blur now takes a `blurMode` param ("normal", "edgeOnly" or
"overlayEdge"; "normal" by default), which its dialog shows as a Mode
menu. Edge Only draws the edges in white on black, and Overlay Edge
draws them in white over the Normal blur. A pixel is on an edge when
one of its four nearest pixels differs from it by more than the
threshold in a channel and its channels, alpha included, add up to at
least that pixel's. The white and the black keep each pixel's
coverage. Normal draws the same pixels as before, and an effect saved
without `blurMode` draws as Normal.
Docs: docs/effects-parity.md, docs/gaps.md, docs/target-app-parity.md.
Copy and Cut made the system clipboard's formats in the frame that ran
them. The formats include the SVG markup and a PNG of the selection at
1 px per pt, up to 4,096 px a side. In a release build on Windows, Copy
of 50,002 rectangles on a 1600 x 1200 pt page took 1.00 s in its frame.
The thread that writes background saves and exports now makes the
formats from a copy of the clipboard, and the app writes them in a
later frame. The same Copy takes 0.18 s in its frame, and the SVG and
PNG are the same bytes. While that thread has a save, an export, a print or
a recovery copy to finish, Copy and Cut make and write the formats at
once. A later Copy or Cut, or text copied in a field or with the Type
tool, drops formats that are not written yet. Until they are written,
Paste on the canvas takes the internal clipboard, and while a text
field or the Type tool has the keyboard the app waits for them.
Docs: docs/mcp.md, docs/target-app-parity.md.
In the Artboards panel, a click or a double-click on a row's options
icon (the page at the right end of the row) chose the Artboard tool on
that row's artboard and opened no dialog. The panel menu's Artboard
Options… also only chose the Artboard tool.
A click on the active artboard's icon, or a double-click on another
row's icon, now opens Artboard Options for that row's artboard. The
first click on another row's icon makes its artboard active, as a
click on the row does. The panel menu's Artboard Options… opens the
dialog for the active artboard. Both run tool.options with tool
"artboard", as a double-click on the Artboard tool's button does, and
they no longer choose the Artboard tool. The icon's tooltip is now
Artboard Options, and the old tooltip's row is removed from the 15
catalogs.
Opened with another tool, Artboard Options did not move the art with a
new X or Y. It now moves the art when the Artboard tool's Move Artwork
with Artboard option is on, as it does while that tool is in use.
Docs: docs/target-app-parity.md.
Effect › Pathfinder left type in the group out of all ten operations,
on the canvas, in Expand Appearance and in exports. The text outliner
returns type as a group with one path per run. `leaves` in
crates/effects/src/group.rs took that group as one leaf, and
`geometry` gives no path for a group. A 100 x 100 pt square grouped
with a 100 pt "H" inside it kept all of its 10,000 pt² after Subtract
and Expand Appearance. In the shape modes a member group with type in
front also lost its paint, because it took the style of the outline
group, which has none.
`leaves` now takes the paths and compound paths of the outline art at
every depth, as it does for a member group, and gives each one the
type's opacity and blend mode. The same square keeps 8,684.88 pt² after
Subtract and Expand Appearance, its 10,000 pt² less the glyph's
1,315.12 pt².
Docs: docs/target-app-parity.md.
perf --deep printed the memory at eight points of its run, and no row
showed memory that the run took and freed between two readings. With
--paths 2000, a release build on Windows printed 430 MB as its largest
row, and the process's peak working set was 612 MB. On Windows the
memory row after rendering the photos also printed its value 3 columns
right of the others: its name is 55 characters, and the name column is
52 characters wide. The tracked row for the photos printed its value 2
columns right of the other tracked rows: its name is 54 characters, and
their name column is 52 characters wide. In the budget table the handle
and segment drag rows printed their values 1 and 2 columns right of the
other rows: their names are 41 and 42 characters, and the name column
is 40 characters wide.
Each memory row now also gives the peak so far: the peak working set
(PeakWorkingSetSize, from the same GetProcessMemoryInfo call) on
Windows, and VmHWM from /proc/self/status on Linux. On macOS the rows
give no peak. A new last row gives the memory after the SVG export and
the save → open → save at the end of the run. The name columns of the
memory rows, the tracked rows and the budget table are 56, 54 and 42
characters wide. The help says on which systems the rows give the peak.
Docs: docs/target-app-parity.md.
The renderer kept the shapes it drew for plug-in effects while the
object stayed the same. When a plug-in was installed for the first
time, installed again or removed, a renderer that had drawn the object
went on drawing the shapes from before the change.
When the plug-in registry's revision changes, the renderer now drops
the effect geometry and the effect art of objects with a plug-in
effect, and the cull bounds of every object that is or holds a symbol
instance or an object with a plug-in effect. Each thread then drops the
pattern tiles whose art has a plug-in effect or shows symbols or
patterns. The renderer adds the revision to the inputs of a raster
when its content has a plug-in effect, or shows symbols or patterns
while the document's symbol or pattern art has one, and to the key of
a live object's evaluation when the object, or an object it shows, has
a plug-in effect.
With other builds running, a temporary release test timed that key for
the 400 live objects in each of three documents at 11 to 609
microseconds a frame, against 9 to 433 before.
Docs: docs/target-app-parity.md, docs/gaps.md.
Object › Artboards › Fit to Artwork Bounds and Fit to Selected Art ran
artboard.fitToArt and artboard.fitToSelection without an index, so
they resized artboard 1 whichever artboard was active. The Artboards
panel menu's Fit to Artwork Bounds already passed the active artboard.
The app now passes its active artboard as the index of the two
commands when a call names none, as it passes the artboard to Align to
Artboard, All on Active Artboard and Paste in Place, in Front and in
Back. That covers the menu items, keyboard shortcuts, the command
palette and MCP's run_command in remote mode. A batch passes its
artboard as the index to their steps that name none, and a recorded
action leaves the index out, so an action resizes the artboard that is
active when it plays. The params texts of the two commands and of
command.batch describe where the index comes from.
Docs: docs/mcp.md, docs/target-app-parity.md.
For Auto Size area type in columns, every text edit, each keystroke of
the Type tool included, laid the text out in one column and then laid
the columns out at that height and at each step of a bisection for the
least height that holds the text. In the release-build timing test
auto_height_in_columns_typing_timing (stories of 5,000 to 50,000
characters at 9 pt in a frame 500 pt wide), a keystroke laid the
columns out 18 to 22 times, and typing at the start of the story of
50,000 characters in 2 columns took 1,670 ms a keystroke.
The bisection now runs on the lines of the one-column layout.
columns_hold computes where those lines would sit in the columns, with
each column's first line at its first baseline below the column's top
(LineInfo::first_baseline, new in vectorcraft-text). The columns are
then laid out at the upper height the bisection ends on and, when they
hold the text there, at the lower one, unless that is the 1 pt it starts
from. When they hold the text at the upper height and not at the lower
one, Auto Size takes the upper height. When taller columns always hold
the text, the old search ends between the same two heights. When the
text wraps around other objects or does not fit the one-column layout,
or when that check fails, the columns are laid out at every step as
before. The same keystroke now takes 292 ms.
Docs: docs/target-app-parity.md.
On each pointer move of an Alt-drag with the Perspective Selection
tool, Session::preview put the drag's snapshot back and ran
perspective.move with copy again, and persp_edit copied every selected
object again. An Alt-drag of a plane widget with the Perspective Grid
or Perspective Selection tool ran perspective.plane.move with objects
"copy" again, and plane_move copied every object on the plane again.
Both commands now take their copies from preview_copies, as
apply_transform does for the Selection and transform tools: the first
preview copies the objects into a clone of the snapshot, and each later
preview of the same objects on the same snapshot maps only those
copies. The copies keep their ids, and the document, the selection,
the undo step and the journal are the same as before. When the stored
grid still lists attachments of its own, as grids saved before
attachments were kept on the objects do, both commands copy on every
preview as before. With 25,000 rectangles attached to the left plane,
a release build of vectorcraft-cli took a median of 424.8 ms for each
Alt-drag move of the objects after the first preview and takes
328.9 ms with this change. An Alt-drag of the plane's widget took
340.3 ms a move and takes 256.9 ms.
Docs: docs/target-app-parity.md.
The canvas drew artboard 1's edge in black, 1 px wide, whichever
artboard was active, and drew every other artboard's edge in gray,
0.6 px wide.
It now draws the active artboard's edge in black and draws the other
artboards' edges in gray, at the same widths. The active artboard is
the artboard that the status bar's navigator shows and that Align to
Artboard and Paste in Place use. With several artboards selected by
the Artboard tool, it is the one the tool draws with handles and its
name.
Docs: docs/target-app-parity.md.
The Layers panel narrowed its rows by name (Search All) or to layers
alone (Show Layers Only).
The panel now has a Filter button at the right of Search All. Its
popup holds Clear All and a check box for each object kind under By
Object (Paths, Shapes, Text, Images, Effects, Groups, Linked Files,
Guides) and for each row state under By Layer (Locked, Hidden). A row
stays when it is of a ticked kind, is in a ticked state and has the
search text in its name, and a section with nothing ticked keeps every
row. The rows that hold a kept row stay too, as they do for the
search. The button is lit while a filter is on. The filter lasts the
session and does not change the document, its open rows or the
selection. ui.layersFilter {objects?, layers?} sets it. The button
shows a new icon, assets/icons/dc-filter.svg, with its row in
ASSETS.md.
The panel lists its rows again when the filter changes, as it does
when the search changes. On a layer of 5,000 groups of 10 paths, a
release build lists the rows for a filter in 2.9 to 3.6 ms.
Docs: docs/mcp.md (Layers panel), docs/target-app-parity.md.
In Presentation Mode the arrow keys moved the selection, and the
active tool received a click and selected or drew as it does in
Normal Screen Mode. No key or click moved the view to another
artboard.
Left Arrow and Right Arrow now go to the previous and the next
artboard, and a click anywhere goes to the next one, through
view.goToArtboard, as the status bar navigator's Previous and Next
buttons do. On the first artboard Left Arrow leaves the view as it
is, and on the last artboard Right Arrow and a click leave it as it
is. No arrow key moves the selection in the mode, and the tool gets
no press, drag or hover, so a click selects and draws nothing. A drag
with the middle button or with Space held no longer pans the view
there; the mouse wheel still zooms and scrolls it.
Docs: docs/target-app-parity.md.
The status bar's artboard navigator showed only the active artboard's
number between its first, previous, next and last buttons, and it had
no list of the artboards.
The number is now a drop-down field that shows the active artboard's
number and name. Its list shows every artboard by number and name, as
the Artboards panel lists them. Choosing one runs view.goToArtboard,
the command the buttons run, which makes that artboard the active one
(and the Artboard tool's, while that tool is in use) and fits it in
the window. The list lays out only the rows in view and at most one
more, and opens with the active artboard's row in view. In a test
build with 1,000 artboards, the two frames that open the list take
0.12 ms and 0.04 ms, and a frame with the list open takes 0.03 ms. The
timed frames are headless and draw only the status bar.
Docs: docs/target-app-parity.md.
With the Artboard tool, the Properties panel showed the active
artboard's name as a label, and no control in the panel opened
Artboard Options.
The name is now a text field. Typing a new name and pressing Enter or
clicking elsewhere renames the active artboard in one undo step
through artboard.setProps, as the Artboards panel's rename does;
pressing Escape or leaving the field empty keeps the old name. A click
on the Artboard Options icon beside the field runs tool.options with
tool "artboard", which opens Artboard Options for the active artboard,
as a double-click on the Artboard tool's button does.
Docs: docs/target-app-parity.md.
In the View menu, Reset Rotate View was the only rotation item. The
status bar's angle menu set its twelve preset angles by writing the
view's rotation itself, and no command took an angle to turn the view
to.
View › Rotate View now lists the status bar's angles (0°, 15°, 30°,
45°, 60°, 90°, 180°, -15°, -30°, -45°, -60° and -90°) and checks the
one the view is turned to. The new UI command `view.rotate {angle}`
turns the view to any angle about the center of the canvas and
returns the angle it set, normalized to (-180, 180] as a Rotate View
drag normalizes it. The menu's items and the status bar's angle menu
run it from one list of angles; the status bar sets the same angles as
before. Without a document the command returns "no document" and the
items are disabled. Run from the command palette, which passes no
angle, Rotate View opens a dialog that asks for the angle, with the
view's current angle filled in.
The de, it, ja, ko, pl, pt-br, ru, uk, vi and zh-hant catalogs have a
new "Rotate View" row. The strings the catalogs cover leave out the
angle labels, as they leave out Type › Size's point sizes.
Docs: docs/mcp.md, docs/target-app-parity.md.
Export for Screens opened on the artboard choice the document last
exported with (all artboards by default), whichever artboards were
selected with the Artboard tool or in the Artboards panel.
While the Artboard tool is in use, File › Export › Export for Screens…
now opens with the artboards the tool has selected checked, and Export
Artboard writes only those. They are the artboards the Artboards panel
highlights, and they are checked even after a Full Document export.
With another tool in use, the dialog still opens on the saved artboard
choice. The other fields show the saved settings in both cases, and the
params text of file.exportForScreens describes the rule.
Docs: docs/control-protocol.md, docs/target-app-parity.md.
Free Distort's dialog showed the four corners as read-only text, and
so did the Appearance panel's inline editor. Only effect.apply and
effect.setParams could set them.
The dialog now shows a box in the proportions of the first selected
object's bounds with a handle on each corner. Dragging a handle moves
that corner, and the preview follows. A handle focused by a click or
Tab moves 1% of the box with each arrow key, and 10% with Shift. Reset
puts the four corners back on the box. Each handle has a
screen-reader name. On the corners row of the Appearance panel's
inline editor, Edit… opens the dialog.
Docs: docs/effects-parity.md, docs/target-app-parity.md.
Show Options and Hide Options were kept in egui's temporary memory, so
the Color panel showed its options again after every restart. No
command set them or the panel's mode.
- `UiState::color_hide_options` holds Hide Options and is saved with the
preferences. Its reader reads `true` as hidden and any other value as
shown, so a bad value leaves the other preferences as they are.
- `ui.colorPanel {mode?, options?}` picks a mode as the panel menu does
(the active color converts to the mode's model as one undo step) and
shows or hides the options. It answers `{mode, options, shown}`. A mode
the menu does not list, `lab` included, answers an error. The panel
menu's Show Options and Hide Options call the command's function.
- `ui.inspect` reports the setting as `ui.color_hide_options`.
Docs: docs/control-protocol.md, docs/target-app-parity.md.
On main, Glowing Edges… is listed in the Effect menu's vector Stylize
submenu, after Drop Shadow, Inner Glow, Outer Glow and Feather. The
reference app lists it in the Stylize submenu of its Photoshop effects,
below the vector effects.
The Effect menu now has a Stylize submenu under Raster Effects, between
Sharpen and Texture, and Glowing Edges is listed there. The menu sends
the effects at the Stylize path that PIXEL_EFFECTS names to that
submenu, and the vector Stylize submenu keeps its six effects. The
effect's id, its catalog entry, its effect.list entry and Apply Last
Effect do not change.
File › Save as Template always wrote a .vctemplate. The save panel
offered only that type, file.saveAsTemplate ignored its format param,
and a name typed with .ait got .vctemplate added. New from Template
already opened a .ait as an untitled document.
Save as Template now also writes a .ait: the PDF-compatible file Save
As writes for .ai, carrying the document. The desktop save panel lists
VectorCraft Template first and the .ait type beside it, and the web
build's Save as Template dialog gets a Format menu with the same two
types. file.saveAsTemplate writes .ait for format: "ait", or for a path
ending in .ait when no format is given, and takes the .ai options;
file.formatOptions answers for ait. .vctemplate stays the default.
Exports and Export As still do not write .ait.
Docs: docs/mcp.md, docs/file-format-parity.md (.ait row),
docs/target-app-parity.md.