Messhias is right that a sampled check cannot promise a run is straight, and my
own write-up overstated what the grid did: I said the fit was "used only where
it reproduces the real map", which five sample positions cannot establish. A
bend narrower than the sample spacing passes between the samples, and the grid
then approves an affine that is simply wrong.
It is not a theoretical gap. A zigzag of amplitude 4 pt and half-period 20 pt,
turning 11.3 degrees (under CORNER_DEG, so the vertices are not corners and
their tangents blend flat), lands its y = 0 vertices exactly on the five sample
positions. All 25 grid points then agree with the affine to the last digit while
the art between them is thrown 4.000 pt off, against the 0.0613 pt tolerance the
grid believed it was enforcing: 65 times over, measured, not estimated.
`Track::is_straight(s0, s1)` reads the answer off the track instead. A bend in
the span is two segment directions that differ, so no bend can hide in it
whatever its size. It also checks the vertex tangents, because `frame` blends
them across a segment unless the vertex is a corner, so a neighbour outside the
span can bend the tangent inside it: directions alone would call `d, d, e`
straight over the first two segments and be wrong. It fails closed on a closed
track's seam, a non-finite bound and a track with no segments.
`AlongMap::is_linear` covers the other half: Between Guides bends the arc-length
map itself, which no amount of looking at the track would reveal.
The grid stays, demoted to a second opinion that can reject and never admit, and
`affine_fit`'s doc comment now says that rather than claiming to be the gate.
Tests. `a_bend_shorter_than_the_sample_grid_is_still_a_bend` builds the zigzag
above, asserts the sampled check accepts it, measures how far out the approved
affine is, and then asserts the track-based gate refuses it; end to end through
`bend` it asserts the aliased bend is subdivided while a genuinely straight run
of the same span keeps the tile's four anchors. Restoring the sampled gate reds
it with `the aliased bend is subdivided, not affine-mapped: 4 anchors`, so the
old code really did lay a flat tile along a waving track.
`straightness_is_read_off_the_track_not_sampled` covers the predicate directly:
collinear segments and their extrapolation, a 0.2 pt detour, a span around it
and a span before it, the blended-tangent case above, the closed seam and a NaN
bound. Writing it caught a bug in my own guard: `f64::min` discards a NaN rather
than propagating it, so `is_straight(NAN, 10.0)` was reporting straight until
the finiteness check moved ahead of the min/max.
FredApps noted a real divergence: the affine route kept whatever the tile held
(it only required bounds) while warp_node drops every node kind but paths,
groups, layers and compound paths, and it did not clear `live` on paths. For a
tile of paths and groups the result was the same, so nothing was visibly wrong,
but "this changes the curves and nothing else" was a wider claim than the code
made: a symbol instance arrived untouched on a straight run and vanished on a
bend.
affine_node now mirrors warp_node's walk through `prepared` and applies one
transform at the end: the same kinds are kept and the rest dropped, `live` is
cleared on every path, an empty path or a group left with no children is
dropped, and strokes are scaled by `width_scale` rather than by the transform's
own factor. The two routes now differ in the tile's curves and in nothing else,
which is what the change was always meant to be.
Test: a_straight_run_keeps_the_same_node_kinds_as_a_bend puts a symbol instance
beside a path in one tile and asserts both routes return the same kinds. On the
old code it reds with `left: ["group", "path", "symbol"] right: ["group",
"path"]`.
Bending flattens every path it maps: warp_path runs kurbo::flatten and
emits SubPath::polyline, so a circle tile is drawn as a polygon. That
shows in the live stroke, not only after Expand Brush Strokes, and it
happens even on a perfectly straight path where the tile needs nothing
more than an affine.
Bending is only non-linear where the track bends. On a straight run the
arc-length map and the normal offset are both linear, so affine_fit
derives the affine from three corners of the art and then checks it
against a five by five grid, including those corners. Within the
tolerance the flattening would have used, the art is transformed
instead, which keeps its Beziers. Outside it, nothing changes: the old
path runs and the result is what it was.
Strokes are scaled by the same factor the bent path uses rather than by
the affine's own, so curves are the only thing this changes.
Corner tiles already took this route, which is why they kept their
curves while the tiles between them did not.
Two existing tests moved with it, both because they read anchor counts
that only held while straight runs were flattened:
pattern_places_corner_tiles told corner tiles from side tiles by
"four anchors", true only because the side rectangles arrived as
polylines. It now tells them apart by a kept handle, which is what
actually distinguishes an ellipse from a rectangle, and additionally
asserts the side tiles keep their own four anchors.
pattern_output_is_capped_across_subpaths_not_only_per_run counted on
sixteen tracks passing the #3594 anchor budget. A tile is about four
times cheaper in anchors now, so the budget engages later; the test
derives how many tracks it needs from what one costs.
Rows the other catalogs already had: new-document presets, color
harmony rules and swatch names, arrowheads, the keyboard shortcut
editor, and the print, PDF and Save for Web options. The workspace
names, OpenType and the perspective grid presets stay in English as
MENU_KEEP_AS_IS requires. Written from the English meaning by a native
speaker.
The GPU start tests release the start file's lock and take it again.
portal_save's mock_portal starts dbus-daemon meanwhile, and until a child
runs its program it holds a copy of every open descriptor, so the lock
outlived the test's own handle: the next start found the file locked, took
it as another copy of the app running and left nothing out. Parallel runs
of the app's tests failed 12 times in 20.
A test-only RwLock: mock_portal holds it exclusively while it spawns, and
each GPU start test holds it shared for the whole test. 0 failures in 70
parallel runs.
File > Document Color Mode and document.open {colorMode} converted the
colors of the layers, symbols, pattern tiles and swatches only. Graph
designs, graphic styles, the art of opacity masks, the tile that
pattern editing's Cancel puts back and the art of brushes kept their
old color model (only a tint of a global swatch in a graphic style or
a mask followed the swatch), so a CMYK document could still apply,
place or print RGB colors from them.
These now convert with the rest, in the same undo step: their solid
colors, gradient stops, text runs and mesh points, nested masks
included. A Hue Shift brush's key color takes the converted color of
the art color it matches, a tint of a global swatch included. The
brush library is stored again only when one of its colors changed. A
tint of a global swatch in any of these places becomes the converted
swatch's color at its tint, as in the layers. The reply's changed
count covers the colors of the layers, symbols, graph designs, pattern
tiles and opacity masks; the colors of graphic styles and brushes
convert without being counted.
Docs: docs/mcp.md, docs/target-app-parity.md.
A desktop portal older than 0.9 ignores handle_token and answers on a
request path of its own. save_file subscribed to that path only after
SaveFile returned it. zbus makes three bus round trips to set up that
subscription. A Response that arrived during them was dropped, and the
save waited forever. On CI the test
the_answer_is_awaited_on_the_path_the_portal_returns waited until the
job limit.
save_file now makes one stream for every Response from the portal
before it calls SaveFile, and takes the first Response whose path is
the returned handle and whose sender is the connection that sent the
reply. A Response that comes before the reply stays queued in the
stream.
The mock portal now creates its message stream before it starts its
thread and before the client connects, so a SaveFile call that arrives
before the thread runs is no longer dropped. A new mode sends the
Response before the reply, and the D-Bus tests fail after 30 seconds
instead of waiting without end.
remote_call_reports_nothing_sent_when_reconnect_is_refused set up the
closed connection once, so only its first run went through the
stale-connection probe. Each run now sets up its own closed connection
before the timed call.
The vectorcraft-mcp tests of the remote backend in tests.rs gave a fake
app 2 or 5 s to accept a connection or to read a request, and waited
2 s for a connection to end or a line to arrive. When a busy machine
held the client's thread for longer, the fake app gave up and the test
failed. These waits now use WAIT (20 s).
remote_connect_fails_fast asserted that a refused connect returned
within 5 s, and remote_call_reports_nothing_sent_when_reconnect_is_refused
asserted that the call returned within 2 s. On Windows a refused local connect
ends at the backend's own 800 ms connect timeout. A connect without
that timeout takes about 2 s. The 5 s bound did not tell the two apart,
and the 2 s bound left 1.2 s for any pause of the thread. Both tests
now bound the connect by FAILED_CONNECT (1.5 s) and run it up to five
times, until one run is under the bound. The second test checks the
error of every run.
While an opacity mask or a pattern is edited, a press on a selected Shaper
original shape now moves it when it is not the first selected object or
shares the selection with shapes of another Shaper Group. An object in
front of the Shaper Group at the pointer is selected instead.
While an opacity mask or a pattern was edited, a Shaper double-click
selected a Shaper Group's original shape without isolating the group's
original shapes. A Selection tool press on that shape then selected and
moved the whole Shaper Group over the art the group shows, and started
a marquee over a part of the shape the group does not show.
A press on the selected original shape now holds it for a move, and
the pointer shows the move cursor over it.
While an opacity mask or a pattern was edited, a Shaper scribble and
Object › Shaper › Release and Expand ended isolation, and a Shaper
double-click on a Shaper Group isolated the group's original shapes.
Each replaced the editing layer's isolation while the document stayed
in mask or pattern editing. A click on the Shaper Group's mode widget
did the same through object.isolate.
A scribble, Release and Expand now keep the editing layer isolated in
these modes, and end isolation as before outside them. A double-click
with Shaper selects the original shape under the pointer without
isolating the group's original shapes, and the mode widget does not
show while a mask or a pattern is edited.
Docs: docs/shaper.md, docs/target-app-parity.md.
object.isolate's errors "type objects can't be isolated", "the object
is hidden or locked" and "missing id" had no row in any catalog, so the
status bar showed them in English in every language. The same was true
of layer.target's "the object is outside the isolated group", the other
isolation-mode error in the commands.
Each of the four messages now has an @msg row in every catalog that
translates all status and error messages and in Bulgarian, Danish,
Indonesian and Dutch. layer.target also gives "the object is hidden or
locked", and several other commands that take an id also give "missing
id", so the same rows translate those errors.
Docs: docs/target-app-parity.md.
The Live Paint Selection tool acted only on the press, so a drag
selected at most the one face or edge under the pointer. The reference
app selects every face and edge a marquee touches, partly enclosed ones
included, and a Shift-drag adds them to the selection or removes them.
A drag of 3 pixels or more now draws a marquee. On release, select.set
selects the faces and edges it touches, or select.none runs when it
touches none. With Shift held at the press, select.toggle runs with
what it touches, and nothing runs when it touches none. A face that holds the whole marquee counts. Select Fills off
leaves faces out, and Select Strokes off leaves edges out.
A click now changes the selection on release instead of on the press,
so Escape between the press and the release, which also removes the
marquee, leaves the selection as it was. The tool no longer highlights
or selects the faces and edges of a Live Paint group that is locked, on
a locked layer or on a locked artboard, or that is outside the isolated
art in isolation mode.
Docs: docs/mcp.md, docs/target-app-parity.md.
On a machine busy with other builds, four tests each failed once and
passed when run again alone.
The two render tests that count the rasters a drag moves draw content
painted with a pattern or showing a symbol, whose raster key holds the
font generation. The fonts change even while a test holds
fonts_steady(): the first lookup by family name catalogs the installed
fonts, and resolving a cataloged family or a fallback character loads a
face. When that happened between the two frames of a drag, the count
was 0. The new with_fonts_unchanged runs a step up to 100 times, until
the generation is the same after a run as before it, and the two tests
take their frames from it. The second render test,
a_moved_group_draws_the_shadow_of_the_instance_art_in_it_while_interacting,
was not seen failing but has the same fault.
The screenshot test's allow_due moved the capture's due time but not
its 8 s wall-clock deadline, so on a slow run the reply was the timeout
error. allow_due now also sets the deadline to f64::MAX; the two timeout
tests set it in the past themselves.
The persistent control server test gave the server 2 s to wait for
each request, 120 ms to read a line and 150 ms to write a reply. It now
gives the line and the reply 1 s each and the wait 30 s. The handler
sleeps 1.25 s, longer than both budgets, so a deadline kept from the
first request still runs out.
The idle expiry test read until the server closed the connection after
its 150 ms idle budget, and the client gave up after 3 s, so a pause of
about 3 s in the server's thread failed it with TimedOut. Every test in
control_server/tests.rs that waits for the server to reply, to close a
connection or to return its slot now waits up to WAIT (20 s), and
line_timeouts() gives the wait for a request and the reply 30 s. The
budgets under test stay 120 and 150 ms (1 s in the persistent test).
The two nested_instances_..._culled_in_bounded_time tests in the
renderer's tests checked that a frame with 2^30 differently placed
symbol instances finishes within 30 s of wall-clock time. On a loaded
machine the frame took longer than that, and both tests failed. The
time bound also passed when the frame did far more work than it should:
with the 4,096 limit on measured placements raised to 65,536, the frame
measured symbol art 131,118 times and finished in under 2 s.
SymbolBoxes now counts how many times measure_art runs a measure on
symbol art (art_measured), and FrameStats::symbol_art holds the count
for the frame's symbol boxes. The tests assert that the frame measures
symbol art at most 20,000 times. It measures it 8,234 times, and 4,160
times when each instance has a Transform effect of its own. The wait
for the frame is 300 s, as a guard against a frame that never ends.
A new test in vectorcraft-doc checks that each symbol's art box is
measured once however many instances of it are measured.