From about 50% zoom on the file of #4155, the pixels drawn are the full 141-megapixel photo,
and in a CMYK document they were separated into ink planes (or colorized, or proofed) whole
for every view: 8 to 9 s of white canvas a zoom step.
When the pixels drawn are larger than a 1,024-pixel tile, their colours are made again for the
frame and part of them is out of view, only the tiles in view are made, each kept under its own
key (`ImageKey::tile`), and drawn together as one pixmap so their edges don't show. The colours
are made pixel by pixel, so the result is the same as for the whole image. CMYK images painting
their own inks are still made whole.
Zooming in to 50%, 100% and 200% on that file went from 9.0, 8.6 and 8.7 s to 0.78, 0.05 and
0.05 s (M3 Pro, one run each), with the same pixels on screen.
With image mips on (the canvas, navigator and layer thumbnails), an image was colorized,
separated into the frame's ink plane and shown through Separations Preview's plates at its
full size, and only then shrunk to the mip drawn. The 141-megapixel image of #4155 made
1.6 GB of planes a frame, more than the image store holds, so every frame made them again.
The mip is now picked first and everything else is made from it, under the keys `kept_mip`
and `nearest_kept` already look up. A CMYK image's own inks are halved with its pixels
(`ink::key_inks`, `images::half_inks`), so it still paints them rather than inks separated
from its displayed colours. Colour bitmap glyphs follow the same order.
On that file the app's first frame went from 9.2 s to 1.6 s and the sharp frame after a pan
from 3.9 s to 8 ms (M3 Pro, one run each). Fine detail now shows its true average: 1-pixel
black and white lines show mid grey, where separating first showed them 44 levels darker.
A linked 1-bit image mask from a .ai file keeps its fill color as a
colorize paint, so the image takes the color once its file is relinked.
Until then the image shows the missing-link placeholder, and the canvas
and PDF export colorized that placeholder too: its light tile turned
nearly transparent and its red cross took the fill color.
ImageBlob::takes_colorize is false for a preview whose pixels are not
grayscale. While it is false, the canvas draws the image and PDF export
writes it as it is, printed separations separate it as an uncolorized
image, and transparency flattening (EPS, PDF 1.3 and PDF/X-1a export,
Flatten Transparency) does not count its paint as transparency. The
paint stays on the image, and a grayscale preview is still colorized.
SVG and EPS export already write only grayscale images colorized.
A move also moves where a stroke's gradient is placed, but the slices of a
gradient along or across the stroke take only its stops and kind. The
moved stroke is now compared with the stroke the slices were made for
with the gradient's placement left out, so a placed gradient no longer
makes its slices again after every move.
A path whose stroke has a gradient along or across it kept its slices under
the node's address, so every nudge made the slices again for the whole path,
although its stroke outline was already reused. The renderer now keeps the key
of a stroke's latest slices under the object's id, the stroke's place and the
tolerance level, and when the object has only moved (same stroke and gradient,
same fill rule, anchors moved by one offset) it draws those slices moved by the
offset, translating only the slices that reach the piece being clipped. Scale,
rotate, an anchor edit or a change to the stroke or its gradient makes the
slices again. The pixels are the same.
The check that reuses a moved path's stroke outline allowed each
coordinate to differ from the moved one by 4 x f64::EPSILON times the
sum of the compared coordinates and the largest coordinate of the first
point, about 2.7 pt near 1e15, so a 0.5 pt anchor or handle edit there
drew the outline made before the edit.
offset_between and anchors_moved now measure the offset on each axis at
the pair of points with the smallest coordinates on that axis, then
check every pair against that offset within 3 units in the last place
of the pair's own coordinates. A first anchor far from the origin no
longer sets the tolerance for points near it.
Tests: 0.5 pt anchor and handle edits at 1e9 and 1e15 (and 0.0005 pt
at 1e9); an edit near the origin of a path that starts at 1e16; a path
whose coordinates shrink with each point, moved by growing amounts.
The outline cache kept a copy of each stroke's line to recognize a moved
path, and made that copy on every miss, so an anchor edit of a long path
paid for copying its whole line. The entry now records whether its outline
was made along a line of the object's own path. On a miss, the anchors of
the earlier object are compared with the new one's, stopping at the first
that differs, and the earlier line is made again from the object and its
stroke only when they match.
Strokes with arrowheads keep the reuse: the check uses the untrimmed path.
The moved-outline key is imported where it is used, and the outline is
stored through one helper, so the change touches fewer lines that other
work on the renderer also changes.
A path object that moves without changing shape drew its stroke by
outlining the whole line again, because the stroke outline cache is
keyed by the node. The renderer now keeps, per object, stroke and
outline width, the latest outline with the line it was made from. When
the new line is that line moved, it draws the kept outline moved by the
same offset, and moves the region a long path was outlined in with it.
Scaling, rotating or editing the path, changing the stroke's width,
join, cap, dashes or profile, a zoom that changes the outline's
tolerance, and a move that takes the view past the outlined region
still outline the stroke again. Only the latest outline of each stroke
keeps its line.
The renderer keeps the path of a path or compound path of 1,024 anchors
or more by object id, with the bounds of its segments and of its miter
spikes in runs of 128 segments. When anchors move and the subpaths and
anchor counts stay the same, the next frame rewrites only the path
elements of the segments that start or end at a moved anchor, and
measures again only the runs that hold them and the runs of the
segments with length before them. Other edits make the path again as
before. The pixels and the cull bounds are the same.
- StrokeLayer::bounds_on is unchanged. The miter tip of one joint
(miter_tip) and the stroke box with given tips (bounds_with_tips) are
new functions beside it, and Appearance::stroked_bounds_by takes the
box of each shaped stroke, so the cull bounds of a kept path use the
kept tips.
- geom::bez::has_length is the predicate reach.rs used for segments
that draw, made public.
Docs: docs/target-app-parity.md.
A dashed stroke of a path with more than 1,024 path elements was outlined
whole in every frame after an edit. It is now outlined in and around the
view like a solid stroke. dash_kept runs the pattern along every segment
and keeps only the dashes and dots on the segments near the view, so they
sit where dashing the whole path puts them and the pixels are the same.
A pattern too fine to draw is still drawn solid, and that solid stroke is
cropped too.
At 1600 x 1200 the frame after one anchor moves takes 14 ms instead of
102 ms on a filled closed path of 50,000 anchors with 6 pt dashes and
4 pt gaps. anchor_bench now includes that document.
The Navigator panel menu's Panel Options… is enabled and opens a dialog with
the view box color (a preset or a custom color; Light Red, today's color, is
the default) and Draw Dashed Lines as Solid Lines, off by default, which draws
dashed strokes without their dashes in the panel's thumbnail only. The
options are kept with the UI state, and ui.navigatorPanelOptions sets them.
RenderOptions::solid_dashes draws dashed strokes as solid lines, including
type strokes and the silhouettes of stroke effects.
The renderer outlined the whole stroke of an edited path on every frame,
so dragging one anchor of a path with many anchors re-outlined every
segment. A stroke line of more than 1,024 elements without dashes or a
width profile is now outlined only for the segments within the stroke's
reach of the view, inflated by the view's own size on each side. The
cached outline keeps that region, and a frame outside it outlines the
stroke again. Closed subpaths that a wide stroke crosses over stay whole,
and a cut closed subpath is walked from a removed segment so no cut lands
where it starts and ends. The pixels are the same.
At 1600 x 1200, the frame after one anchor moves takes 28 to 30 ms
instead of 103 ms on an open path of 200,000 anchors, and 10 ms instead
of 30 ms on a closed path of 50,000 anchors (the new anchor_bench
example: cargo run --release -p vectorcraft-engine --example anchor_bench).
A compound path was assembled from its members in every frame it was
drawn, and Node::stroke_path grew its buffer by doubling, copying the
path into ever larger buffers for a compound path of many anchors. The
renderer's geometry cache was swept only once it held more than 1,024
entries, so while an object was dragged (a new node, and layer, in
every frame) the old copies stayed alive with all their members.
Now:
- a compound path is drawn from the path the renderer keeps for it in
its geometry cache, as a plain path already was, and is assembled
only when the node changes;
- Node::stroke_path sizes a compound path's buffer up front from its
members' segment counts (the same elements, in the same order);
- unused geometry entries are dropped after a few frames however few
entries there are.
Tests:
- crates/render/src/tests/memory.rs: the second frame draws the kept
path and the same pixels as the first and as a new renderer; after a
five-frame move the renderer no longer holds the compound path drawn
before it. The test fails without the renderer change.
- crates/doc/src/node/tests.rs: a compound path's path is its members'
paths in order (open, closed, empty and single-anchor members).
Not timed; vectorcraft-cli perf was not rerun.
A live object reached the renderer without passing the isolated-object check that groups go
through, so an isolated repeat was dimmed with the art around it. Tests cover leaving isolation
(double-click outside, exitIsolation), undo/redo of an edit inside, and a repeat of a group.
Entering symbol editing mode from an instance puts a copy of the symbol's art on the editing layer in the instance's place, and the instance stays in the document. The view drew the instance dimmed under the copy, so art with transparency showed darker, and its edges showed a dimmed fringe.
The renderer now leaves that instance out of the screen view while the editing layer is isolated, with its masks and effects, and in Outline view inside a clip group. Exports and thumbnails draw it as before.
The canvas keeps a hairline visible by raising a stroke's weight to half a device pixel. A width profile multiplies that weight, so a 1 pt stroke with a 6x width point swelled to twice its size at 25% zoom and shrank back on zooming in. The minimum now goes to the profile's widest point instead; plain strokes are unchanged.
Pattern tiles were drawn with the default render options, so an image
whose pixels cannot be decoded drew nothing inside a pattern fill even
when the render marks undecoded images, as the canvas does. Tiles now
follow the caller's mark_undecoded_images setting, and the tile cache
keys on it.
An opacity mask with such a pattern now treats the mark as part of the
mask, as it does for an undecodable image outside a pattern.
The renderer drew the root node of an opacity mask's art, of placed symbol
art, of an art brush piece, of an Image Trace source image and of a node
thumbnail without its own opacity mask. Make Opacity Mask uses the top
object as the mask art, so a mask made from an object that already had an
opacity mask ignored that inner mask. These roots now draw through the
same masked group as any other node, still drawn whatever their visibility
and bounds, and an incomplete inner mask still leaves out the outer object.
With undecoded-image marks off, an image in an opacity mask's art that
failed to draw left a hole in the mask. Without Clip, the area outside
the mask art counts as white, so the masked object showed in full.
The renderer now notes an image that fails to draw while mask art is
drawn and leaves the masked object out, as PDF export does since #4765.
This covers images in pattern tiles and in cached effect and shadow
rasters, which remember the failure and report it again when they are
reused. Nested masks pass the failure outward, and View Opacity Mask
shows no coverage for such a mask. An image still waiting to be decoded,
or drawn with the undecoded-image mark, keeps today's behavior.
A knockout element with one opaque Normal solid fill (a path or
compound, no mask, effects or isolation) is drawn once under
AntiAlias::None, including the internal hard-edge frame of Art
Optimized supersampled exports, instead of being erased and added back
through per-element backdrop layers. Analytical antialiasing keeps the
existing compositor and rounding.
Exact-pixel tests compare against the general knockout path for paths
and compound holes across precision, supersampling, translucent parents
and copied backdrops, and check that the backdrop rasters are skipped;
translucent fills keep their knockout compositing.
# Conflicts:
# docs/roadmap.md
# docs/target-app-parity.md
Envelope Options stored Anti-Alias and Preserve Shape Using, but nothing that
draws an image read them: a distorted image was always smoothed and always
cut into clip groups.
- With Anti-Alias off, each piece of a distorted image is marked pixelated
(saved as `pixelated`). The renderer draws it with nearest-neighbor
sampling and without mips, SVG export writes `image-rendering: pixelated`,
and PDF export writes it without /Interpolate.
- With Preserve Shape Using Transparency, each piece is the image with a
white opacity mask in the piece's shape instead of a clip group. The
renderer paints an image whose mask is one plain white path straight into
that path, as it does for an image in a clip group, when the path lies
inside the image's frame.
Docs: docs/mcp.md, docs/target-app-parity.md.