Commit Graph
18 Commits
Author SHA1 Message Date
dotCooCoo a723f7359a Reuse text outlines across moves 2026-11-06 08:51:36 +07:00
dotCooCoo d88f5eeed9 Render: a moved path reuses its gradient stroke slices
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.
2026-11-05 13:58:16 +07:00
dotCooCoo 815671b3a2 Render: at very large coordinates an edited path is not taken for a moved one
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.
2026-11-05 11:29:39 +07:00
dotCooCoo a4e74ed0f0 Render: stroke outline reuse leaves the shared imports and the dashed-outline branch as they were
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.
2026-11-05 10:26:08 +07:00
dotCooCoo e6331e559a Render: a moved path reuses its stroke outline
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.
2026-11-05 10:26:08 +07:00
dotCooCoo d9d33c9e4b Render: the frame after anchors of a long path move updates only the segments that touch them
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.
2026-11-05 06:28:22 +07:00
dotCooCoo 4c51f853c1 Render: node renders follow the anti-aliasing option
render_node_with and render_node_thumbnail_with drew into a new context
without setting its aliasing threshold, so AntiAlias::None still gave
smooth edges. Both now set the threshold from the options, as a page
render does.
2026-11-04 18:28:35 +07:00
dotCooCoo 676cc0ee8e Render: a long stroke is outlined only near the view after an edit
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).
2026-11-04 16:55:30 +07:00
dotCooCoo f15a16f2c1 Render: placed-document bitmaps draw undecoded-image marks when the view does
On screen, a placed document is drawn from a bitmap made in the
background, and that bitmap was rendered with the default options. An
image inside the placed file whose pixels cannot be decoded therefore
drew nothing in the bitmap, even though the canvas marks such images
with a crossed box. The bitmap now follows the frame's
mark_undecoded_images setting, the bitmap cache keys on it, and a
bitmap standing in for one still being made has the same setting.
2026-11-04 14:56:36 +07:00
Ahmed Gomaa 1d011ab8ad Render: a compound path's members are assembled once and kept with the node
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.
2026-11-03 16:03:40 +07:00
dotCooCoo 4c32032d34 Part of #3602: a node thumbnail reports the size it is drawn at
render_node_thumbnail_with draws into a context of size.clamp(1,
MAX_CONTEXT_SIDE) pixels but reported width and height as the requested
size, so a size of 0 gave a 0x0 image holding 4 bytes. The view was
also built from the requested size, and the fit scale (size - 2) /
extent was 0 or negative at sizes 1 and 2, so the art drew nothing or
mirrored.

The thumbnail now reports the side it draws, builds the view from it,
and drops the 1-pixel margin when the side is too small to keep one.

Test: node_thumbnail_reports_the_size_it_draws.
2026-10-31 23:29:33 +07:00
dotCooCoo 8aa8bb4d9d Render: a mask art root, symbol art root or thumbnail keeps its own opacity mask
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.
2026-10-31 19:43:15 +07:00
dotCooCoo 44bac0d7cf Render: leave out an object whose opacity mask lost an image
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.
2026-10-31 07:33:49 +07:00
bflatastic f1211e7c89 Separations Preview: hidden plates leave raster images too (#4153) 2026-10-30 04:25:27 +07:00
dotCooCoo 8dd9b962b8 Tests: the nested-instance culling tests count the symbol art a frame measures
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.
2026-10-29 13:00:25 +07:00
dotCooCoo ab69d8f6d9 Envelopes: Anti-Alias and Preserve Shape Using change how distorted images draw
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.
2026-10-29 11:07:57 +07:00
Dylan M. Taylor 7e44df55a9 Make renderer and SVG module dependencies explicit 2026-10-28 05:03:00 +00:00
aiongg e0db48ba39 refactor(render): move lib.rs items to {blobs,bounds,clock,frame,geometry,node_checks,options,paint,renderer,renderer/images,renderer/nodes,renderer/paint,renderer/text,text_geom,threads}.rs 2026-10-26 22:01:25 +00:00