With the Document Library palette, each pixel took the library color
nearest in plain Oklab distance. A 50% gray lies nearer in that distance
to a saturated blue of the same lightness than to black or a light
cream, and a muted brown is also nearer to the blue than to either. With
a black, cream and blue library, both traced as the blue.
This changes how Document Library matches pixels: each pixel now takes
the library color nearest with differences in hue and chroma (Oklab a
and b) counted three times as much as lightness. The same measure picks
which library colors are used. Neutral and muted tones take a neutral
library color, and the other palettes are unchanged.
With Method Overlapping, each color layer covered every pixel of the colors
above it in the stack. A small dot or square on a background was then traced
once in its own color and again, at the same bounds, in every color between
the background and it, and with Shapes on each copy became its own live shape.
Those layers also ended on edges they never touched and showed through the
anti-aliased rim there.
Each layer now leaves out every 4-connected part of its stacked area that holds
no pixel of its own color. This changes the result of many Overlapping traces,
the photo presets included: fewer paths (High Fidelity Photo 121 to 69.6 on its
own quality inputs, Low Fidelity Photo 37.1 to 26.1) and no rim, around a
shape, of a color that sits apart from it. One existing test that counted the
extra copies now expects one layer per area, and another that compared layer
pixel counts now checks that the bottom layer covers the whole image.
Two shapes that overlap by less than the region precision (0.01 pt) leave
their overlap out of the Shape Builder's regions as a sliver. Merging the
regions on both sides of it united only the picked faces, so the result
was a compound path with a hairline gap where the overlap had been.
A merge now takes in each dropped sliver next to a merged face when only
the merged faces' shapes fill it, both from the arrangement and from the
planar map used when open paths are selected. A thin cutout that no shape
fills stays a hole. What is left of a shape after the merge takes in a
sliver only where the shape covers it and the merged shape does not take
it in, so the two do not overlap. Pathfinder operations and Live Paint do
not use the merger and are unchanged; Live Paint's planar map does not
test which shapes cover a sliver.
When both sides of a 1.5 pt bar have a point level with a click 0.1 pt
from its end, the point of each side nearest the click is the end of a
segment, so the sides were not tried and an Alt-click there touched the
end edge.
A segment end now also counts when the segment is square there to the
line to the point, so splitting a side at the click gives the same
answer as an unsplit side.
A point 0.1 pt from the end of a 1.5 pt bar is nearest the end of the
bar, and the line from there through the point runs along the bar, so
the region did not count as narrow and an Alt-click there touched the
end edge.
A region now counts as narrow at a point when it is no more than twice
the tolerance across from any outline segment within the tolerance whose
point square to the line to the point lies inside the segment, as well
as from the nearest point of the outline. Segment ends other than the
nearest point are not tried, so a point added on an edge does not change
the result near a corner.
In erase mode the tool tests each point for a line piece or an edge
within the tolerance before the region under it. Every point of a region
narrower than twice the tolerance is that close to its edges, so an
Alt-click, the Alt hover and an Alt-drag over a thin overlap touched its
edges and not the overlap, and the overlap was left behind.
A point in a region now touches the region when the region is no more
than twice the tolerance across there, measured from the nearest point
of its outline through the point to the outline on the other side. This
covers a drag at any angle, a drag that starts or ends in the region,
and a thin shape inside another or on its own. Near an edge of a wider
region, the edge is touched as before.
shapeBuilder.merge built the planar map of the selection again, on the
thread that runs the command, although the Shape Builder tool had just
built the same map for its hover. The tool now returns its map from a
new Tool::shape_builder_map, and the command uses that map when its
inputs are the merge's inputs and its gap is the merge's gap; otherwise
it builds its own as before. The tool's map has the outline edges, so
erasing and splitting a stroke use it too.
CodeRabbit on #4080: `needed` summed footprint pieces before insertion
deduplicated them, so on a degenerate grid (ten collinear lines, 4,096 pieces
each in ten cells) every item exceeded the budget and went to the scanned list,
making the per-cell assertion vacuous. The cells are now collected in piece
order and consecutive repeats dropped before the budget check and the filing;
the horizontal fixture asserts `rest` is empty and that the lines are filed.
docs/target-app-parity.md: the Path operations area row, the Shape Builder
row and the evidence list record the bounded index (scores unchanged).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014gCpjL3yUhaH3jRp7ksufh
Follow-up to #4021. The grid filed each piece of a segment under its cells
without checking, so a degenerate grid (collinear horizontal lines: cells a
hundred points wide and a hair tall) stored one segment ~400 times per cell,
and coincident vertical segments (one column, a row per segment) filed every
segment under every row: a quadratic number of references, about a GiB for
16,384 segments within the Shape Builder's limit.
Now a segment is filed once per cell (its pieces come in order, so the cell's
last entry is enough to check), and the grid holds a budget of references
(64 per item plus four rows' or columns' worth, `reference_budget`); the
items past it go to the scanned list, so the index stays O(n·sqrt(n)) in
memory and every lookup still finds every segment through its point.
Regression: `rect_index_holds_a_bounded_number_of_references` (fails without
the change: 4,100 references per cell for ten collinear lines).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014gCpjL3yUhaH3jRp7ksufh
A group counted as a Live Paint group when its name started with "Live
Paint". A plain group named, for example, "Live Painting" was then read
as one. With it and another shape selected, the Shape Builder found no
regions in it, and a merge on the other shape deleted the group and its
paths. Make Live Paint took the group as a Live Paint group to merge
into, and its paths did not become sources.
A Live Paint group now also needs its sources group, the group named
"Live Paint Sources" that Make Live Paint puts in it. A plain group with
such a name is traversed like any other group.
draw_path, draw_shape and object.setProps take opacity in percent (0-100),
but the document.inspect / document.node summary behind doc_inspect,
inspect_document and vectorcraft://object/{id} reported the stored 0-1
fraction under the same name. An agent that read 0.55 and wrote it back
got a nearly invisible object.
The summary now adds opacityPct (0-100, rounded to 1/100 of a percent)
beside the unchanged opacity, and the tool, resource and command
descriptions say which scale each field uses. Round-trip test in the mcp
crate: draw at 55 %, read 55 back from doc_inspect, inspect_document and
the object resource, write it back unchanged.
## Summary
`vectorcraft-cli convert` to PDF now passes `preserveEditing: false`, matching
a command-line converter expectation while Save As PDF in the app still uses
the VectorCraft Default preset. `--preserve-editing` opts in and adds a JSON
warning when the native document attachment is written.
## Tests
Integration test in `vectorcraft-cli` checks default PDF output has no editing
attachment and `--preserve-editing` embeds it with a warning.
Closesstorytold/vectorcraft#3627
Co-authored-by: nrq6993-a11y <nrq6993-a11y@users.noreply.github.com>
## Summary
`vectorcraft-cli convert` to PDF now passes `preserveEditing: false`, matching
a command-line converter expectation while Save As PDF in the app still uses
the VectorCraft Default preset. `--preserve-editing` opts in and adds a JSON
warning when the native document attachment is written.
## Tests
Integration test in `vectorcraft-cli` checks default PDF output has no editing
attachment and `--preserve-editing` embeds it with a warning.
Closesstorytold/vectorcraft#3627
Co-authored-by: nrq6993-a11y <nrq6993-a11y@users.noreply.github.com>
Use function paths where a closure only forwarded its argument
(PoisonError::into_inner, OsStr::to_str, FindRange::is_empty), and drop
redundant 'static on associated and module consts. No behaviour change.
An undefined number (--) in `-- -- angle length Bh` is read as a radial
gradient with no highlight, and XC (a color space name after a linked
image's file name) is ignored like its neighbours. Either one used to make
the whole editing data unreadable, so the file opened as its PDF part and
lost its layers and live text. A -- anywhere else is still an operator the
reader doesn't know.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
wasmi compiles each function of a plug-in when it is first called and,
by default, charges the run that calls it 7 instructions for each byte
of the function. The host hands a run its budget in 20 M-instruction
slices and resumes it when a slice runs out. But when a slice runs out
while a function compiles, wasmi returns an error from the call, not an
out-of-fuel pause, and the host reports it as Limit("instruction
budget"). So a plug-in's first run could fail with most of its budget
left, depending on where the slices fell, and the next run, with its
functions compiled, succeeded.
StitchCraft's PES exporter hit it on a design of 98 × 148 mm. In
VectorCraft 0.17.0's host its first export failed with 10 G and with
275 G instructions alike, and its second succeeded, taking under 1 G.
Run on wasmi directly, with the host's slices, the first export failed
in its 36th slice ("required_fuel=174139"), and with compiling free it
finished in 49.
Compiling now costs nothing: wasmi's fuel costs with no cost for
translating or validating, and its default cost for copying. A run pays
for its own instructions only, as many on its first run as on the next.
Compiling every function when the plug-in loads (CompilationMode::Eager)
also fixes it, but loads a 1.4 MB module in 42 ms instead of 10.
The new test fails without the change: a run that takes 1,040
instructions once its functions are compiled failed within them on a
plug-in just loaded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhvnnnD5fTFupK8WWq35un
With the Type tool at a caret and no characters selected, choosing an
item in the Character panel menu's Character Direction submenu set the
direction of every character in the type object. The three items are now
disabled until characters are selected.
The Symbol Stainer mixed the color of type characters painted with a
global swatch but kept the swatch name and tint, so the characters'
paint named a swatch whose color it no longer had, and a later edit of
the swatch could put the unstained color back. Their fill and stroke now
become plain colors, as a path's paint does.
With nothing selected, Cmd+/ and Cmd+Alt+/ ran the commands, which put
"select objects or give ids" in the status bar, and the command palette
listed both as available. Without ids and without an Appearance panel
subject (a targeted layer, group or object, or a selected one), the two
commands are now off in menus and the palette, as the panel's buttons
are, and their keys run nothing. Called with ids, they still add to those
objects.
The Paintbrush drops a sample less than a pixel from the last kept
point, and a release there keeps that point. The new test moves the
pointer inside Close Paths When Ends Are Within while the kept point
stays outside it, and checks that the cursor shows no closing ring and
that the release leaves the path open.
buffered_partial_control_line_also_expires and
stalled_control_line_is_refused_with_the_reason used short_timeouts(),
which gives a connection 150 ms before its first byte and 150 ms to write
a reply. When a test thread pauses longer than that, the server closes
the connection without a reply, the client reads end of stream, and the
test fails parsing an empty line (CI run 40212833958 on #4152).
The two tests now use line_timeouts(): the same 120 ms line deadline,
with idle and write budgets of 10 s. The other short_timeouts() tests
are unchanged.
Picks up the startup prompt without the glyphs a font may lack (storytold/craft-libs#343, #344): the close mark is painted and the version pill is ASCII, so apps that panic on a missing glyph no longer take the prompt down.
Part of tracking issue storytold/craft-libs#319
* feat: startup update prompt modal and direct self-updating
The update flow (check at start or on request, the startup prompt, the self-update worker) is craft-update's Updater; the app only supplies the suite, its close path and its status window. The check at start is opt-in for the desktop app, and the Windows update helper runs before argument parsing.
Ref: storytold/craft-libs#319
* refactor(updates): hold the install back while documents are unsaved
* fix(updates): drop the manual window's custom id; pin craft-update to craft-libs main (db19c37)
* fix(updates): address review feedback
Wake the UI when a manual check is queued from a menu callback; the update prompt owns the keyboard while it is open (LightCraft); Download opens through the desktop URL hook (FilmCraft); the Help menu dispatches through menus::invoke (FigCraft); print jobs and comment drafts hold an install back (WordCraft); Check for Updates is a registered command (GridCraft); an install holds back while documents are unsaved.
* fix(updates): localize the update status messages
* build: resolve the craft-update pin after rebasing