160 Commits
Author SHA1 Message Date
bflatastic 6bc0222074 #1723 follow-up: pattern tiles are drawn again after a brush edit or new Color Settings
A tile was kept while the pattern's art and what it shows were unchanged,
but a brush the art is stroked with lives in the document's other data, and
every tile is drawn through the color settings. A tile whose art reads more
than itself now also remembers the document's other data, and every tile
remembers the color settings it was drawn through (both weakly).
2026-10-14 12:27:04 +07:00
dotCooCoo c44ece3113 Render: the art prepared for Separations Preview and Overprint Preview is kept between frames
With Separations Preview on, the renderer copied every node of the
document in every frame and separated each of its colors again. With
Overprint Preview on, it copied each object with an overprinting fill
or stroke in every frame, with the groups and layers that hold it. The
copies were new Arcs, so the geometry, stroke outline and raster
caches missed for each of them in every frame. In a release build,
drawing a 2880x1800 frame of a 50,000-path document again with the
same view and Separations Preview took 795 to 908 ms.

The renderer now keeps the art it prepared and what it was made from,
and in the next frame it uses that art again for each layer, symbol
art and member that has not changed. Only the objects that changed are
prepared again, with the layers and groups that hold them. All of the
art is prepared again when Discard White Overprint changes and, with
Separations Preview, when the plates shown, the proof setup, the
swatches, the Spot Colors option or the color settings change or a
profile is registered (cms::profiles_generation). The renderer now
draws that frame again in 98 to 107 ms, with the same pixels.

Docs: docs/target-app-parity.md.
2026-10-14 11:22:41 +07:00
bflatastic 0efcaeb558 Importers: a brush that draws itself by name no longer crashes
A scatter brush whose art is an instance of a symbol stroked with that brush, or any brush whose
art is painted with a pattern whose tile is stroked with it, drew itself without end: a stack
overflow in a hand-written native file. Brush pieces already strip the brushes of their own
strokes; now what they draw by name that would draw a brushed stroke (a symbol instance, or a
pattern paint, also through the symbols and patterns those use) is left out too, in one place
(`stroke_pieces`, which every renderer and exporter calls with the document).

Probed the rest of item 2 of the depth follow-up with hand-written hostile inputs, kept as tests:
graph designs of deep art and of a symbol holding itself, Affinity groups nested to and past
both of its reader's caps, DXF blocks inserting themselves, EMF/WMF saving states without end,
and plug-in manifests and output nested 30,000 to 100,000 deep. None of them overflowed.
2026-10-14 10:13:45 +07:00
bflatastic 5688294d56 Fix #1683: a template layer dims its images by Dim Images, not the whole layer
The canvas faded a template layer as a whole by a fixed half and ignored its
Dim Images option, so unchecking it or setting another percentage changed
nothing, and the layer's paths were faded with its images. A template layer's
images are now dimmed to its Dim Images percentage (50 % when Layer Options
makes it a template), shown at full strength with the option off, and its
vector art is drawn as it is.
2026-10-14 09:02:28 +07:00
bflatastic c348b384aa Fix #1723: a pattern's tile is drawn again when what its art shows changes
A tile was kept while the pattern's own art, tile and tiling were the ones it
was drawn from. Art that shows a symbol, is painted with another pattern or
holds an image or type doesn't change when those do, so the pattern kept
drawing the old symbol (in every renderer on the thread: the tiles are
shared). Such a tile now also remembers the document's pattern and symbol
art, image pixels and fonts, and is drawn again when they differ. A raster
effect on an object painted with a pattern reads all of that too, as one on
a symbol instance does.
2026-10-14 09:02:28 +07:00
bflatastic 48041c8cd3 Fix #1706: a shadow or glow too large to cache is filtered within the pixel budget
The fallback for a drop shadow, outer glow or inner glow whose raster the
shadow cache refuses rendered the view plus the blur's spread at full
resolution, capped only at 65535 pixels a side: a 1000 pt blur at 3200 %
spreads 48000 pixels, so the surface was up to 16 GB whatever the window's
size. The region is now planned by filter_region, which renders it at a lower
resolution (as filtered content already is) to stay within PIXEL_BUDGET.
2026-10-14 09:00:42 +07:00
dotCooCoo 7975b07b22 Render: type with transparency, a blend mode or raster effects keeps its layout between frames
Only type at full opacity, in Normal blending and without object
effects took its glyph outlines from the geometry the renderer keeps
for each type object. Type at less than 100% opacity or in a blend
mode, and the art drawn for type with a drop shadow, glow, blur or
other raster effect, were laid out again in every frame.

draw_text now takes the geometry from text_geom_of when the type it
draws is fx_owner, the node draw_arc is drawing, and the frame does
not snap type to pixels. While the effect-art cache keeps type art,
that art is drawn as fx_owner. The geometry is laid out again when the
type is edited or the fonts change, and frames with Type Optimized
anti-aliasing lay type out as before. On 8 threads at 2880 x 1800, a
page of 1,000 type objects at 80% opacity takes 13 ms a frame instead
of 23 ms, with the same pixels.

Opaque type kept the layout of earlier frames when a symbol it shows
as an inline graphic changed size and the document's inline graphics
had not been resolved again. text_geom_of now lays type out without
the cache when the text it is given is not the node's own, such as the
copy that Document::inline_resolved makes with the new size.

Docs: docs/target-app-parity.md.
2026-10-14 06:01:10 +07:00
Fabio William ConceiçãoandCursor b6d14b1fd0 Part of #394: Appearance of Black › Printing / Exporting draws pure CMYK black as RGB black
Co-authored-by: Cursor <[email protected]>
2026-10-14 05:11:55 +07:00
bflatastic 37174ac306 Render tests: the symbol art bounds test holds the fonts steady (#1744) 2026-10-14 04:56:30 +07:00
dotCooCoo d9bd9b134d Symbols: instances of imported symbols are measured by their art
Every symbol instance was measured as its transform applied to the
20 × 20 square around the origin, which symbols made in the app keep
their art in. The SVG and DXF importers keep symbol art as drawn, so
the box of an instance of an imported symbol did not match its art.
The SVG Star instance at x 20, y 30 draws from (20, 30) to (40, 50)
and was measured from (10, 20) to (30, 40). Align moved such an
instance by the wrong amount, clicks and marquees beside its art
selected it, and the canvas, exports and prints left it out when its
art reached the view or the page and the square did not.

An instance is now measured by its symbol's art, which
Document::symbol_boxes looks up in the document each time
(Node::geometric_bounds_with, visual_bounds_with and
reach_bounds_with). With Use Preview Bounds on, the strokes in the art
count at their own weight, as the canvas draws them, and
object.setBounds counts them at that weight when it sizes an instance
or an object that holds one. When strokes do not scale and a width or
height of 0 or more is smaller than what a path's stroke adds,
object.setBounds now gives the path no width or height, as it does
for an instance whose art has strokes; before, it mirrored the path.
With Use Preview Bounds on, object.setBounds now measures the margins
that strokes add object by object, the members of groups and the
objects in symbol instances' art included, and solves for the width,
height and reference point asked for when the width and height are 0
or more. Before, it measured the margins of the whole selection and
could miss the size or the reference point asked for when strokes did
not scale and the objects' strokes added different margins, and when
strokes scaled and the width and height were out of proportion, even
for one path. A clip group, a Shaper Group, a compound path, type, and
a group or instance with effects or an opacity mask of its own are each
measured as one object, and so is an instance in perspective or one
turned or slanted relative to the bounding box, other than by quarter
turns. When the selection holds more than 4,096 objects to measure this
way, each selected object is measured as one. Instances inside symbol
art are measured by their own symbol's art. An instance is measured as
the square when its symbol is missing, when its symbol or a symbol
nested in its art holds an instance of itself, and when symbols nest 32
deep in its art.

Align and Distribute, the Transform panel, the bounding box and the
selection outline, clicks, the marquee, document.inspect and
document.node, Fit to Artwork Bounds and Fit to Selected Art, canvas
culling, thumbnails, the SVG filter region of an instance with a
raster effect, raster effect images, Export Selection, the SVG of an
artboard, printing, and the PDF, EPS, EMF/WMF and DXF writers use the
new box. The renderer also culls an instance by the effects and
brushed strokes in its art, and it culls a blend, envelope or repeat
by the brush art of what it holds and the effects in the art of the
instances in it. It culls an instance with geometry effects of its own
by the effects in its art after those geometry effects reshape the art.
It measures the effects in a symbol's art once for each symbol and
linear part of the transform that places it. For symbols whose art
holds instances, it measures at most 4,096 placements a frame, and at
most 4,096 more for each pattern tile it rasterizes and each placed
document it draws. Past that, it estimates the reach from the symbol's
reach with no transform, which it measures once. When the drawn
document's symbols change, the renderer drops the cached bounds of the
instances and of the objects that hold them.

The PDF and EPS writers, the SVG of an artboard and Skip Blank
Artboards grow the box of an instance whose art holds brushed strokes
by 16 times the widest stroke (at least 1 pt) of the objects in that
art that have a brushed stroke, plus 8 pt, as the renderer grows the
cull bounds of a brushed path. They keep an instance whose brush art
reaches the page. The SVG of an artboard also keeps an object that
holds an instance when what the canvas paints for it, the effects in
the symbol's art and the instance's own included, reaches the artboard.

Not yet: other commands that measure objects, such as Select › All on
Active Artboard, Transform Each and Convert to Artboards, still measure
an instance by the square.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-14 04:56:30 +07:00
dotCooCoo f9cbcfb046 Render: the ink planes of CMYK documents with transparency are composed on several threads
A CMYK document that shows transparency is drawn as two ink planes,
and ink::compose turned them into screen colors one pixel at a time on
one thread. At 2880 x 1800 that took about as long as drawing one
plane of 50,000 paths. The renderer also searched a CMYK document for
transparency in every frame.

compose now splits the frame into bands, as many as the renderer has
threads and no more than one per 65,536 pixels. The calling thread
composes the first band and a scoped thread composes each other band,
with the same per-pixel code. The renderer keeps the answer of the
transparency test with the layers, symbol art and pattern art it was
found for, held weakly, and tests again only when the document holds
other art. With Overprint Preview on, a document whose layers or
symbols hold an overprinting fill or stroke is still tested in every
frame. FrameStats::ink_threads reports how many threads composed a
frame. On 8 threads at 2880 x 1800, a page of 300 shapes at 20% to 90%
opacity takes 30 ms a frame instead of 55 ms, and 50,000 paths with
one at 99% opacity take 258 ms instead of 361 ms, with the same
pixels.

Docs: docs/target-app-parity.md.
2026-10-14 04:56:30 +07:00
dotCooCoo 2ce6f90522 Render: Outline view draws paths and clipped art from geometry kept between frames
In Outline view the renderer made a new path from the path data of
each path and compound path it drew, in every frame. In Preview the
renderer draws plain paths from the path it keeps for each object
between frames. In Outline view the members of a clip group were
culled by bounds measured again in every frame, and the bounds of a
group inside a clip group were measured from every path in it. The
outlined shapes of an Image Trace object were culled and drawn the
same way.

In Outline view the renderer now draws paths and compound paths from
the paths it keeps for them, and culls the members of clip groups and
the outlined shapes of Image Trace objects by the bounds it keeps for
them. The live-effect geometry of those members is kept between frames
too. The frames have the same pixels as before.

With 50,000 paths at 2880x1800 on 8 threads, an Outline frame at 400%
took 47.1 ms on main and takes 16.7 ms when the paths are the members
of one clip group. It took 36.6 ms and takes 26.3 ms when the paths
are paired into compound paths.

Docs: docs/target-app-parity.md.
2026-10-14 04:56:30 +07:00
bflatastic caa80fb213 Render tests: the live object tests take the font locks (#1688) 2026-10-14 04:56:30 +07:00
dotCooCoo 6504dc7da2 Render: live objects are evaluated again when patterns, symbols, fonts or color settings change
The renderer kept the evaluation of each blend, envelope and repeat
while the object and a hash stayed the same: the addresses of the art of
the symbols the object showed and of the patterns it painted with, and
the patterns' tile bounds. When anything else the evaluation reads
changed, such as a pattern's tile type, symbols in symbol art, the
fonts or the color settings, a renderer that had drawn the object went
on drawing the old evaluation. The quads of a gradient mesh were kept
the same way after a change to the color settings, and the art drawn
for a live object with an effect of its own was kept by the object's
address alone.

resources_key now also walks the opacity-mask, symbol and pattern art
it finds. It hashes what it finds, the font generation when it finds
type or a placed document, and the color settings. Mesh quads are made
again when the color settings change. The art drawn for an object's
effects is made again when the hash changes, if it was made from
evaluated live objects.

With other builds running, a temporary release test timed the hash for
400 live objects at 17 to 506 microseconds a frame, against 10 to 16
before.

Docs: docs/target-app-parity.md, docs/gaps.md.
2026-10-14 04:56:29 +07:00
dotCooCoo 78980c8190 Render: the canvas no longer draws its picture back for each non-isolated group that blends
A transparency group whose members blend with the art below it, and a
masked object with a blend mode, are drawn over a copy of that art. For
each such group in each frame, the renderer rendered everything drawn
so far into a new pixmap the size of its context (on the canvas, the
whole view), started that context again and drew that pixmap back into
it as an image, which the next copy and the frame rendered again. At
2880x1800, a document with 50 translucent groups, each holding a
Multiply rectangle, took 299 ms a frame on 8 threads.

Every render that composites in 8 bits, the canvas included, now keeps
that picture and renders what the context draws next over it. The
backdrop is still cut from the picture. The 50 groups take 65 ms a
frame, and the frames have the same pixels. Renders that composite in
floating point (raster exports, Save for Web, Object › Rasterize and
Crop Image) still draw the picture back.

Docs: docs/target-app-parity.md.
2026-10-14 04:56:29 +07:00
bflatastic 819e53fe92 Render tests: the stroke outline tests hold the fonts steady (#1667) 2026-10-14 04:56:29 +07:00
dotCooCoo 9f0a9a362b Render: objects that share an appearance keep their own stroke outlines between frames
The renderer kept the outline of a stroke under the address of the
stroke in the object's appearance, the bits of the width and the
tolerance level, and used an entry only when the entry held the object
being drawn. The slices of a gradient along or across a stroke were
kept the same way. Objects can share one appearance until one of them
changes it. Copies made by Duplicate Selection share the original's
appearance, gradients included. For objects without a gradient fill or
stroke, so do the art the renderer places for unstained symbol
instances, the copies a live repeat draws and copies made by
edit.duplicate. In each frame, the objects that share an appearance
replaced each other's entries. The renderer therefore outlined their
strokes again in every frame.

The two caches now key each entry by the address of the object as
well, and each object keeps its own outlines from frame to frame. Art
that the renderer makes again in every frame keeps the old key: symbol
art that placed_art does not keep, and the evaluations of live objects
and the art of objects with effects that their caches do not keep. The
copies in such art replace each other's entries as before.

vectorcraft-cli bench of 2,000 unstained instances of a symbol of
eight paths took 45.7 ms a frame on main and 31.1 ms with this change,
at 2880x1800 on 8 threads, and a grid repeat of the same paths, which
draws 2,000 groups, took 45.0 ms on main and 27.2 ms with this change.
The peak working set of a bench run on the instances went from 88 MiB
to 122 MiB. In the tests, each frame draws what a new renderer draws.
2026-10-14 04:56:29 +07:00
Dan Green 96d5a14738 Render tests: tests that add fonts no longer run alongside the frame cache tests
tests_fontchange adds fonts to the shared font database, which makes every renderer lay type
out again; a tests_memory test counting the type it kept between frames then failed when one ran
alongside it (about half of the runs of the render tests). The font tests now hold a lock alone,
and the frame cache tests hold it shared.
2026-10-14 03:55:41 +07:00
dotCooCoo 35d6aa0a16 Render: pattern tiles at different scales are kept between frames
The renderer kept one tile per pattern name and ink plane, and used it
only at the raster scale it was made at: the device scale rounded up to
a power of two and capped. Two objects that paint one pattern at scales
for which the raster scale differs, such as a pattern scaled to 25%
with Transform Pattern Tiles next to the same pattern at 100%, replaced
each other's tile, and every frame rasterized both tiles again.

Tiles are now kept per pattern name and raster scale, within the tile
cache's budget of 128 MiB per thread and ink plane. When the budget
fills, the cache drops the least recently used tiles first. A tile is
still used only while the pattern's art, tile, tile type and overlap
are the ones it was made from. At 760%, with a pattern of 576 stars in
a 288 pt tile on a rectangle that fills the view, and the same pattern
at 25% on a square, release builds of vectorcraft-cli bench at
2880x1800 took a median of 26.0 ms a frame on main and 8.8 ms with this
change, with 8 threads.

Docs: docs/target-app-parity.md.
2026-10-13 12:58:05 +07:00
Marco Arnulfo 791bd91d5c Strokes: a zero-length subpath draws its round or projecting cap as a dot (fixes #1613) 2026-10-13 12:58:05 +07:00
bflatastic 670946ef75 Palette index: rustfmt 2026-10-13 10:52:38 +07:00
Ayanprogrammer11 cca3a63e4a Palette nearest-colour lookup: branch-and-bound index over the linear scan
Indexed export (GIF, PNG-8, BMP, TIFF) finds each pixel's nearest
palette entry with a linear scan, and the k-means refinement in median
cut does the same once per cell per pass. With the 256-entry palettes
this code produces, that is 256 distance evaluations per query.

PaletteIndex is an exact nearest-neighbour kd-tree over the palette,
balanced at the widest weighted axis, with tight channel bounds in every
subtree so branch-and-bound can skip whole boxes. Small palettes (12 or
fewer entries) keep the scan, which is cheaper than tree traversal.

Results match the linear scan exactly, including ties: an equal-distance
entry with a smaller index still wins. The bound uses the same weighted
squared f32 form as dist() rather than a transformed space, so rounding
cannot change which entry is chosen.

Three tests: randomized parity against the original scan across palette
sizes spanning the cutoff, three weight vectors, duplicate colours and
fractional query points; explicit tie-breaking cases; and an opt-in
benchmark comparing 100k queries at both, behind -- --ignored.
2026-10-13 10:52:38 +07:00
BFlat 6a16627de6 Merge pull request #1583 from storytold/fix/1534-guides
Fix #1534: guide paths take the guide colour and style, and a dragged selection snaps to ruler guides with Smart Guides on
2026-10-13 09:09:09 +07:00
bflatastic 6939eff2f9 Fix #1534: paths made into guides take the guide colour and style of Preferences 2026-10-13 08:51:43 +07:00
Gert Pellin 05fdd15b45 Render: effect rasters drawn after a pattern fill keep their place and size
fx.rs draws blur, shadow and glow rasters as image paints that map 1:1
onto their rect, but only set the geometry transform: the paint
transform a pattern fill (or a radial or freeform gradient) left on the
context still applied, so in supersampled renders (raster exports) the
effect came out tiny and displaced. Both draws now reset the paint
transform first.

Fixes #1536
2026-10-13 08:51:37 +07:00
Ayberk A. 15cef94ba6 3D and Materials: Inflate takes Twist, Taper and Cap
The public 3D and Materials panel options list Depth, Twist, Taper and Cap
for every 3D type; Inflate had only Depth.

- Twist (0° to 360°) and Taper (1–100 %) turn and scale the back about the
  shape's centre, as Extrude & Bevel's do: a puffed back is turned and
  lowered in proportion, and the sides follow in slices.
- Cap: Solid closes the back; Hollow leaves it off, so the inflated front is
  a shell open behind. The documentation doesn't say what Cap means for
  Inflate; this reading is stated in docs/three-d.md.
- The dialog's Object tab shows them in the documented order (Depth, Twist,
  Taper, Cap), then Volume and Both sides.

At 0 % volume a twisted, tapered Inflate has the extrusion's outline from
every side (tested).

A twisted, tapered, hollow Inflate draws on the canvas exactly as it
exports, and expands to faces with their own ids (tested). ROADMAP and the
parity pages list the new options; the 3D score stays at ~25 %.
2026-10-13 05:22:00 +07:00
fastmikey99andClaude Opus 5.5 d084af162f Render: decode print-size rasters, and say when an image is missing from an export
The renderer decoded images with the decoder's default 512 MB allocation
limit, so a 300 ppi roll (6614 x 59055 px, 1.56 GB as RGBA) that opens and
exports to PDF fine drew nothing in a raster export - a fully transparent PNG,
exit code 0, no warning. It now decodes with the allowance opening has (2 GB,
512 MB on the web), shrinks an image past a pixmap's 65535 px side to fit
instead of dropping it, and counts any image it still can't decode: a raster
export that drew fewer images than it should says so in its warnings.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 05:20:57 +07:00
bflatastic a787fb487c Fix #1486: a template layer stays in Preview in Outline view
In Outline view a template layer drew as outlines like any other, so a
picture placed to be traced turned into a box. It now keeps drawing in
Preview (dimmed as before) while the other layers are in outline.
2026-10-13 04:00:17 +07:00
Ayberk A. 8a3458a974 Effect › 3D and Materials: Extrude & Bevel, Inflate and Rotate
The three 3D types besides Revolve, as live effects in the three-d crate,
drawn as shaded vector faces by the canvas, SVG/PDF export and Expand
Appearance alike, from the public 3D and Materials documentation:

- Extrude & Bevel: depth (0-2000 pt), twist, taper, solid or hollow caps,
  and bevels in six original cross-sections (classic, rounded, cove, step,
  ogee) with width, height, repeat, spacing, inside and both sides.
- Inflate: the shape as a membrane under pressure (a Poisson solve held at
  zero along the outline and holes), so it rises vertically from the edge
  and rounds without creases; volume, depth and both sides. Steep cells
  near the outline are refined without cracks.
- Rotate: every fill and stroke turned in 3D and shaded as one plane.

All three share Revolve's view and lighting parameters, the rotation and
light gizmos (turning about the art's centre) and a dialog with Object,
Rotation and Lighting tabs, with Position presets (Front ... Isometric
Bottom, our own values). An object takes one 3D effect. Inflate cuts the
grid cells that straddle a hole's edge with pathops, hence the new
pathops -> three-d layer order.

Docs: docs/three-d.md, mcp.md, and the 3D rows of the parity docs.
2026-10-13 00:35:07 +07:00
dotCooCoo f4e72fe6a5 Render: raster effects are made again when patterns, symbols, images, settings or fonts change
The renderer kept the rasters of an object's drop shadows, outer glows
and filtered content (Gaussian Blur, Feather and the Photoshop-style
filters), and drew them again while the object, the zoom and rotation
and the ink plane stayed the same. The content drawn into a raster also
reads things outside the object: patterns, symbols, brushes, images,
document, layer and color settings, the knockout a group inherits, the
fonts, and the options the frame is drawn with. When one of them
changed, a renderer that had drawn the object went on drawing the
raster it had kept, made from the old values.

Each kept raster now records what its content was drawn from: which of
those things the object reads, found by walking the object when the
raster is made, and a hash of their values then. The renderer draws
the raster only while that hash is the same in the current frame. Once
a frame, the renderer hashes the document's patterns, symbols and
images. It hashes their art and pixels by the addresses of their
shared values, and it keeps weak references to those values while a
raster made with them is kept, so no other value can take one of those
addresses. A filtered raster also records the color channels its
filters split, and under De-Interlace the raster resolution.

A temporary release test timed the inputs of 400 kept rasters at about
10 microseconds a frame.
2026-10-13 00:34:41 +07:00
bflatastic 1168c319b0 Close the hole whenever the inner offset of a closed stroke has crossed over
The check added for #1396 dropped the inner contour only when the path itself fell in the hole, which takes a stroke more than twice as wide as the shape. Between one and two times the size (a 30 pt ellipse with a 50 pt stroke), and on rectangles with round or bevel joins, the hole stayed.

An offset contour is now left out when no point of it lies half the weight from its subpath: it is then nowhere the edge of the stroke. Subpaths are judged one by one, and only when the stroke is at least as wide as the subpath's bounds, so ordinary strokes take the same single call as before.
2026-10-12 20:22:07 +07:00
dotCooCoo 412e83c825 Render: the placed art of symbol instances and inline graphics is kept between frames
The renderer drew each symbol instance by copying its symbol's art,
staining the copy for a stained instance and moving every object in it
by the instance's transform, in every frame. The copies were new
nodes, so the caches the renderer keys by a node's address (cull
bounds, path geometry, glyph layouts, live-effect geometry) missed for
every object of every instance, and the copies stayed in those caches
until they were pruned. Stroke outlines missed as well, because their
cache uses an entry only when the entry holds the node being drawn.
The renderer copied and moved the art of inline graphics in type the
same way.

The renderer now keeps the art it places in a frame cache with a
64 MiB budget, under the address of the symbol's art, the transform,
whether strokes scale with it, and the instance's stain. When the art
is a group or a layer without an appearance of its own, the renderer
copies none of it in a frame whose instances and symbols did not
change, and the objects in it hit the caches keyed by their own
address (cull bounds and paths, glyph layouts, live-effect geometry).
The stroke-outline cache keys a stroke by its address in the
appearance. In the art placed for an unstained instance, each object
without a gradient fill or stroke shares its appearance with the same
object in the symbol's art. When a symbol has two or more unstained
instances, the renderer therefore still outlines the strokes of those
objects again for each instance in every frame. Moving or restaining
an instance, or editing its symbol's art, places that art again, and
the other instances keep theirs. Other art is placed again in every
frame, as before, and so is art that holds a symbol instance, type
with an inline graphic, an image, a placed document, a blend, an
envelope, a repeat or a raster effect. Art that holds type is placed
again when the fonts change.

vectorcraft-cli bench of a symbol of eight paths (three filled and
stroked, two only filled, three only stroked) shown by 2,000
unstained instances took 51.6 ms a frame on main and 32.3 ms with
this change, at 2880x1800 on 8 threads; the same paths in 2,000
groups took 26.0 ms. The peak working set of a bench run at that size
went from 150 MiB to 83 MiB. In the tests, a renderer that keeps
placed art draws the same pixels as a new renderer, and instances
draw the same pixels as groups of their placed art.
2026-10-12 18:56:39 +07:00
dotCooCoo a9a7a4cddf Fix #1257: a dragged object reuses its filter and shadow rasters
Each pointer move of a drag applies the move to the interaction's
snapshot, so the dragged object is a new node in every frame. The
renderer kept the rasters of an object's drop shadows, outer glows and
filtered content (Gaussian Blur, Feather and the Photoshop-style
filters) only for the same node, so every frame of a drag ran those
effects again. On main, a moved frame of texture_drag_perf (a CMYK
rounded rectangle at 190.07% and DPI 1.5 in an 1800 x 1500 view) took
112 to 197 ms in a release build.

The canvas now renders with RenderOptions::interacting while a drag or
a dialog's preview is in progress. When an object misses its cached
raster in such a frame, the renderer takes the raster an earlier frame
made for an object with the same id, slot and ink at the same zoom and
rotation, made by the same effects from content of the same size (to
within 0.000001 pt). It draws the object's content into a raster
placed as that one was and compares the pixels byte for byte (only the
alpha under a shadow or glow). For Feather it also compares the points
of the clip outline, to within 0.000001 pt. When they match, the old
raster is drawn at the new place, rounded to a whole pixel, as a pan
draws a cached raster. Content under De-Interlace never takes a moved
raster, and a raster made over moved rasters counts as moved itself.

A frame rendered without interacting never draws such a raster, or a
raster made over one: the frame after the drag runs the effects again
and draws what a fresh renderer draws. The canvas's cache key includes
whether an interaction is in progress. On the desktop, the canvas
sends the frame after an interaction to the render worker when the
frame shown drew moved rasters. Each cached raster keeps the content it
was made from, within the existing 512 MiB budget. The pixels kept
with a filtered raster take as many bytes as the raster, and the alpha
kept with a shadow or glow takes a quarter as many as the shadow.

texture_drag_perf now renders a drag's frames as the canvas does, then
the frame after the drag, and that frame once more with every raster
cached. A moved frame now takes 20 to 26 ms, close to the 18 to 25 ms
of the cached frame; the frame after the drag takes 110 to 189 ms.

Docs: docs/target-app-parity.md.
2026-10-12 18:56:38 +07:00
Aaron GuoandClaude Opus 5.5 b29e271b4a Part of #394: Appearance of Black › On Screen shows pure CMYK black as RGB black
Preferences › Appearance of Black › On Screen was stored and never read.
Display All Blacks Accurately (the default) shows pure CMYK black, black
ink alone at 100%, through the working CMYK profile, a dark grey, as the
canvas always did. Display All Blacks as Rich Black now shows it on the
canvas as RGB black: fills, strokes, type, gradient stops, effect colours
and pattern tiles, and in CMYK documents drawn by ink planes a pixel
whose inks come out as black ink alone. Rich blacks, black tints and
placed images show the same either way; exports and printing are
unchanged (Printing / Exporting is a separate option).

The renderer gets RenderOptions::rich_black, painted with a new
Ink::RichBlack (screen colours with pure black as RGB black) and applied
by ink::compose. The canvas sets it from blackOnScreen and keeps it in
its cache key, so changing the preference redraws.

A headless frame test reads the canvas raster: K100 shows its profile
grey by default, RGB black with rich, a rich black unchanged, and grey
again with accurate. A render test covers the opaque and ink-plane paths
and checks that rich blacks, tints and images don't change.

Docs: docs/mcp.md (blackOnScreen), docs/target-app-parity.md,
docs/gaps.md, docs/ui-parity.md.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-12 14:50:00 +07:00
arhammusheer 5beac4f122 #1244: smaller objects: Node 352 -> 152 bytes, NodeKind 200 -> 80, and a cheaper undo estimate
Rarely set fields move into one optional boxed NodeExtras, and large NodeKind payloads (images,
live shapes, blends, envelopes) are boxed. Every object, and every undo step that copies it,
shrinks: a 50k-path move keeps ~24.5 MB per step instead of ~40 MB. Files stay byte-identical
(Node keeps its flat JSON through a borrowed Serialize and a from-wire Deserialize, no flatten),
and loading is slightly faster. Undo and redo reuse their step's size estimate instead of
walking the document again (8.3 -> 5.6 ms for a 50k-path move).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VmMmc1J8XeNLxhRihWv9cc

# Conflicts:
#	crates/engine/src/cmd/graph.rs
#	crates/svg/src/export.rs
2026-10-12 11:02:32 +07:00
arhammusheerandClaude Opus 5.5 c4214be9d7 #1244: render caches bounded by bytes, evicting the least recently used instead of clearing
Text geometry, clips, clip paints, effect art, brushes, live effects, pattern tiles and freeform
grids were capped by entry count and cleared wholesale on overflow, so a document past the cap
(e.g. more than 4,096 type objects) recomputed every entry on every frame. A FrameCache keeps
per-entry byte sizes and last-used frames, drops the least recently used entries of earlier
frames, and sweeps stale ones. Text geometry keeps its glyph outlines once instead of twice.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VmMmc1J8XeNLxhRihWv9cc
2026-10-12 11:02:17 +07:00
arhammusheerandClaude Opus 5.5 fe8a6983c5 #1244: one shared, byte-budgeted store for decoded images; render caches that sweep and stay bounded
Decoded images were kept per Renderer at full resolution and never evicted, so the canvas,
render worker, thumbnails and panels each held their own copy (96 MB per 24 MP photo).
They now live once in a process-wide LRU store keyed by content, with optional mip levels
(RenderOptions::image_mips, off by default so exports stay identical). Thumbnail renders
now sweep their caches, and shadow/effect rasters share a 512 MiB budget and match by Arc.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VmMmc1J8XeNLxhRihWv9cc
2026-10-12 11:02:17 +07:00
Ikki Nishihara d0569b7fe7 perf: テクスチャ効果の距離判定を軽くする #1257 2026-10-12 07:25:58 +07:00
arhammusheerandClaude Opus 5.5 0724aafbb6 #1172: share an object's appearance with its undo history until it changes
Every edit of an object copied its whole appearance (its fills, strokes and effects, about
540 bytes for a basic object) into the new node, though a move leaves it as it is. Over 45
whole-selection moves of 10,000 paths, undo history grew by 589 MB; now 349 MB (41 % less), and
a nudge or a drag of the selection takes 12 ms instead of 15.

- Node::appearance is a vectorcraft_doc::Shared<Appearance>: copy-on-write. A clone shares it,
  and the first change through DerefMut copies it (Arc::make_mut). Reads, writes, equality (also
  against a plain Appearance) and the saved form are those of the Appearance itself, so call
  sites read and change it as before. Sites that build or assign a whole appearance add .into(),
  and the few that need a plain copy take (*n.appearance).clone().
- Node::transform changes the appearance only when it has gradients to transform, and pattern
  paints are transformed only when there are some: a move no longer copies an appearance it
  doesn't change.

The workspace suite (5,301 tests) and clippy pass with the change; the doc and engine suites
pass again after the transform guard.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VmMmc1J8XeNLxhRihWv9cc
2026-10-11 23:27:21 +07:00
arhammusheerandClaude Opus 5.5 f8064ba3ce #1172: rasterize with up to 8 threads instead of 4
The automatic rasterizer thread count was one per core up to 4. On a 10-core Apple M2 Pro, 8
workers are faster (9 are slower again), and vello_cpu draws the same pixels with any number:

- fit page, 50,000 paths: 66.7 → 56.6 ms; after a pan 66.4 → 51.2 ms
- 400 % zoom, 50,000 paths: 17.8 → 15.7 ms (back under its 16 ms budget)
- fit page, 100,000 paths: 137 → 105 ms; 400 % zoom 33.0 → 25.8 ms

Machines with 5 cores or fewer keep their count, and Preferences → Performance and
VECTORCRAFT_RENDER_THREADS still override it. pixels_do_not_depend_on_the_thread_count renders
a page of live effects and 400 overlapping translucent objects with 0, 1, 4 and 8 workers and
gets the same pixels.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VmMmc1J8XeNLxhRihWv9cc
2026-10-11 23:26:53 +07:00
arhammusheerandClaude Opus 5.5 add27cb8fc #1172: keep live-effect geometry across frames
A node with live geometry effects (Offset Path, Outline Stroke, Warp, Transform, Roughen…) had
them computed again on every frame: Offset Path's curve offsetting and boolean sweep for every
object, every time the canvas redrew. Rendering 2,000 paths that each have Offset Path, warm:
175 → 11.8 ms.

The renderer keeps each node's effected geometry, and each appearance item's own geometry
effects on it, keyed by the node's Arc identity like its other per-node caches. The entry keeps
the Arc alive, so a hit is exact and an edited object (a new node) gets new geometry. Only the
node draw_arc is drawing uses the cache: its path comes from the node alone, so its effects give
the same result every frame. Entries not drawn for three frames are swept like the rest.

cached_effect_geometry_draws_the_same_pixels renders a page of Offset Path, Roughen + Drop
Shadow, Warp + Outline Stroke and Transform objects (with item-level Offset Path) cold, warm,
after a pan and with a fresh renderer, and gets the same pixels each time; after an edit, the
warm renderer draws what a fresh one does. Keying the cache by node id instead of identity fails
it. The render suite (216 tests, golden images included) passes unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VmMmc1J8XeNLxhRihWv9cc
2026-10-11 23:26:53 +07:00
bflatasticandClaude Opus 5.5 255fefe71f Render: a shape far beyond any page is left undrawn, never crashing the app
A property test of the importers (import_fuzz) crashed CI with
STATUS_STACK_BUFFER_OVERRUN: a shape with coordinates in the 1e20s from a
mutated file made vello_cpu either panic in flatten_simd on a rayon
worker (rayon then aborts the process, so the engine's panic guard never
sees it) or ask for a 132 GB allocation, which aborts too. A malformed
file could end the app and lose the user's work.

draw_shape now leaves a shape undrawn when its control points, through
the view, aren't finite or reach past 1e10 pixels, or a visible stroke
is that wide (MAX_DEVICE). A document's art stays within 4 million
points, 2.6e9 pixels at the closest zoom, and vello was checked safe
with 90,000 random shapes within 1e9 pixels and 30,000 within 1e12.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 16:13:30 +07:00
Biromedic 1726c600d2 Fix #1140: gradient stops blend colour and opacity apart on the canvas, as the exports do
The canvas built its peniko gradients premultiplied (peniko's default), while
Gradient::sample, the PDF export (shading plus SMask) and SVG viewers blend
colour and opacity separately: a transparent white stop into black drew grey
127 halfway instead of 191. Opaque gradients are unchanged.
2026-10-11 16:00:36 +07:00
skylar taylor-barrick 0c57b1d5d2 UI.6: reconcile shared docking with native panel sets 2026-10-10 19:28:31 -06:00
skylar taylor-barrick ed14640756 UI.9: integrate shared docking and preserve panel workflows 2026-10-10 14:49:35 -06:00
bflatasticandClaude Opus 5.5 178f99d60e #983: supersample Art Optimized edges in exports
Asset Export, File › Export, Save for Web and Object › Rasterize drew
each shape with its own smoothed edge, so where two shapes met edge to
edge (Divide, Shape Builder, abutting art) both half-covered the pixels
on the line and the background showed through as a thin ghost border.

With Art Optimized (the default) these now supersample, as the option's
name in Illustrator says: the art is drawn with hard edges at four times
the size (three or two times for large images, within a 40-megapixel
budget and the renderer's 65,535-pixel side) and each block averaged
into one pixel. Pixels shared by two shapes are fully owned by one or
the other at the finer size, so no seam is left, and the outer edges
are still smooth. The canvas keeps shape-by-shape smoothing for speed;
None and Type Optimized are unchanged.

RenderOptions::supersample turns it on (exports and Rasterize set it).
A render test checks two triangles sharing a diagonal: a seam with
shape-by-shape smoothing, none supersampled, and the factor's limits.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 03:48:03 +07:00
alpha-rock c36b6976e3 M9: add live Revolve with artboard rotation and lighting controls 2026-10-10 23:44:46 +07:00
bflatasticandClaude Opus 5.5 30ba774e6f Part of gap 2: Effect › Texture (Craquelure, Grain, Mosaic Tiles, Patchwork, Stained Glass, Texturizer)
Six raster effects on the pixel-filter pipeline
(vectorcraft_effects::pixel::texture), under a new Effect › Texture
submenu between Sharpen and Video in the Raster Effects section:

- Craquelure (crackSpacing pt 2..100, default 15; crackDepth 0..10, 6;
  crackBrightness 0..10, 9): the object on relief plaster that cracks into
  plates about Crack Spacing apart (a Voronoi network over jittered points)
  and along the quarter, half and three-quarter contours of its softened
  tones. Depth sets how dark and wide the cracks are and how high the
  plates' bevels and the tones stand (lit from the top left); Brightness
  how brightly the surface is lit.
- Grain (intensity 0..100, 40; contrast 0..100, 50 = unchanged; grainType
  Regular, Soft, Sprinkles, Clumped, Contrasty, Enlarged, Stippled,
  Horizontal, Vertical, Speckle; default Regular): coloured 1-point grains,
  smooth ones, clumps, 2-point grains, streaks along rows or columns, dark
  specks; Sprinkles and Stippled use the background colour, white.
- Mosaic Tiles (tileSize pt 2..100, 12; groutWidth 1..15, 3; lightenGrout
  0..10, 9): irregular tiles bevelled at their edges, with sunken grout half
  a point wide per step, lightened as Lighten Grout says.
- Patchwork (squareSize pt 0..10, 4; relief 0..25, 8): squares filled with
  the colour around their centres, raised to heights that follow the
  highlights and shadows, a little more or less at random.
- Stained Glass (cellSize pt 2..50, 10; borderThickness 1..20, 4;
  lightIntensity 0..10, 3): single-coloured panes leaded in the foreground
  colour (black, half a point per step), lit from behind at the object's
  centre and fading out towards its corners (PixelSpace gains `extent`).
- Texturizer (texture Brick, Burlap, Canvas, Sandstone, default Canvas;
  scaling % 50..200, 100; relief 0..50, 4; lightDirection Bottom, Bottom
  Left, Left, Top Left, Top, Top Right, Right, Bottom Right, default Top;
  invert): the object on a surface in relief. The four surfaces are height
  fields generated in code (bricks in stretcher bond, plain weaves coarse
  and fine, octaves of smooth noise); no image files. Load Texture is left
  out.

Every pattern lies in document space around the object's centre, so the
result is the same at any zoom or raster resolution, moves with the object
and is identical on every redraw (hash noise of cell indices); slopes are
taken in document units. Work per pixel is bounded (a 5 x 5 cell search,
four or five height samples, box blurs); parameters are clamped to their
ranges and finite-checked. The Pixelate helpers (Place, the cell search,
the softened colours) are shared, Mezzotint uses the shared smooth noise,
and the effect dialogs' dropdowns translate their labels in an "effect"
context where the plain word means something else (Soft, Horizontal and
Vertical are type settings in Japanese and Chinese).

Catalogue rows with parameter docs, outsets and reaches, menu entries,
dropdowns (Grain Type, Texture, Light Direction), translations, tests
(parameters and clamping, cracks and contours, ten distinct grains, tiles
and grout, squares in relief, leaded panes and the centre light, the
textures lit and inverted, the same at twice the resolution and when
moved, canvas, multithreaded, PDF and SVG, the command sweep with junk
parameters, the dialogs and the menu) and ROADMAP.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-10 19:31:47 +07:00
bflatasticandClaude Opus 5.5 c71af3079c Part of gap 2: Effect › Video (De-Interlace, NTSC Colors)
Adds the Video group to the raster effects, after Sharpen:

- De-Interlace…: Eliminate Odd or Even Fields, Create New Fields by
  Duplication or Interpolation. The fields are the rows of the document's
  raster grid (Document Raster Effects Settings › Resolution, carried to
  the filters as PixelSpace::line) counted from the top of the page, so
  the result is the same at any zoom and moves with the page. A line
  taken out copies the line above (the one below at the top), or takes
  the average of the two.
- NTSC Colors (no options, applied at once): colours a composite
  television signal can't carry, with luma plus chroma past 110 % of white
  or luma minus chroma below -20 % in YIQ, have their chroma cut to fit,
  keeping their luma. Saturated yellow, cyan and pure red lose some
  saturation; legal colours stay as they are.

Catalogue rows with parameter docs, dialog dropdowns, menu, translations,
tests (effects, canvas, command sweep junk, menu) and ROADMAP.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-10 17:05:56 +07:00
bflatasticandClaude Opus 5.5 9bad0a24ca Part of gap 2: Effect › Pixelate (Color Halftone, Crystallize, Mezzotint, Pointillize)
Four Photoshop-style raster effects on the pixel-filter pipeline
(vectorcraft_effects::pixel::pixelate), under a new Effect › Pixelate
submenu between Blur and Sharpen in the Raster Effects section:

- Color Halftone (maxRadius 4..127, default 8; channel1..channel4 screen
  angles -360..360, defaults 108/162/90/45): each colour channel is
  screened at its angle in square cells sized so a dot of Max. Radius
  fills one; each cell's dot covers the share of the cell that is the
  channel's coverage-weighted mean there (white stays white, black
  black). RGB documents screen R, G, B (channels 1 to 3); CMYK documents
  screen C, M, Y, K inks (channels 1 to 4), also when they are drawn ink
  plane by ink plane (the K plane uses channel 4).
- Crystallize (cellSize 3..300, default 10): every pixel takes the
  colour at the nearest of one jittered point per cell (exact Voronoi
  over a 5 x 5 neighbourhood, pruned by distance).
- Mezzotint (type: Fine/Medium/Grainy/Coarse Dots, Short/Medium/Long
  Lines, Short/Medium/Long Strokes; default Fine Dots; a dropdown in the
  dialog): each channel is turned fully on or off against its own random
  threshold pattern, so the object becomes pure colours.
- Pointillize (cellSize 3..300, default 5): one dot of random size per
  cell at a jittered point, coloured by the softened object there and as
  opaque as the object at its centre, stacked in random order over a
  white canvas where the object is.

Lengths are in points (Scale Strokes & Effects scales them) and every
pattern lies in document space around the object's centre, so the
result is the same at any zoom or raster resolution, moves with the
object, and is identical on every redraw (hash noise of the pattern's
cell indices, no state). Work per pixel is bounded whatever the
parameters, which are clamped to their ranges and finite-checked; the
halftone's cell grid is capped by the raster's size and a screen finer
than 3 pixels leaves the image as it is (its average). PixelSpace gains
`channels` so the renderer tells the filters what the channels hold.

Catalogue rows with parameter docs, outsets and reaches for cropping,
menu entries, dialog labels (Max. Radius, Channel 1-4, Cell Size), the
Mezzotint Type dropdown, translations, and tests: parameters and
clamping, tone and coverage, CMYK inks, polygon crystals, pure-colour
mezzotints with ten distinct patterns, dots on a white canvas, the same
result at twice the resolution and when moved, the canvas, PDF and SVG
through the engine, the command sweep with junk parameters, the dialogs
and the menu.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-10 15:42:24 +07:00