Commit Graph
238 Commits
Author SHA1 Message Date
Taha-Mahmoodi 087e38aafc Render: a dashed brush stroke keeps the layered path for object opacity
Follow-up to #3697 (CodeRabbit): a brush stroke is drawn as separate
pieces (draw_brush), so folding object opacity into its colour isn't exact.
A non-dashed brush stroke already counted as one paint before #3697; to
keep that change's scope, a dashed stroke counts as one paint only when it
has no brush. dashed_stroke_opacity_folds_into_its_colour checks that a
dashed brush stroke isn't folded; it fails without the change.
2026-10-23 04:14:37 +07:00
Taha-Mahmoodi 32072c77fa Fix #2788: a dashed stroke with object opacity folds the opacity into its colour instead of compositing a canvas-wide layer 2026-10-23 03:22:55 +07:00
Taha-Mahmoodi 2bad646821 Gradients: a Perceptual gradient mixes its colours in OKLab, and SVG and PDF write it as an image
Illustrator 30.0's gradient Method, from the public documentation
(https://helpx.adobe.com/illustrator/desktop/paint-and-fill/create-and-edit-gradients/control-perceptual-interpolation-in-gradients.html):
Classic or Perceptual; perceptual interpolation matches colour transitions to
how the eye perceives differences between colours, and perceptual gradients
are rasterised when saved as PDF or SVG.

- Gradient gains `method` (Classic | Perceptual; serde default Classic,
  written only when Perceptual, so older files open as Classic and Classic
  files are written as before). The colour crate's OKLab helpers open up for it.
- Perceptual mixes neighbouring stop colours in OKLab (sample, and
  sample_keep_model for the stroke meshes, Expand and blends); opacity stays
  linear. Renderers that mix linearly get stops on the perceptual curve,
  placed by halving each half segment where a straight line strays more than
  0.4 of a colour level (a nearly straight segment needs none). Classic
  gradients expand exactly as before.
- draws_as_pixels includes Perceptual gradients, so SVG and PDF write a
  Perceptual fill or stroke as an image at the document's raster effects
  resolution, clipped to what it paints. A gradient along or across a stroke
  stays slices (PDF no longer draws it as a within-stroke image).
- With Dither: a dithered Perceptual fill is the perceptual mix with dither's
  noise (the canvas's dithered grids are keyed by method too); SVG and PDF
  write it as an image with the noise, and a dithered Perceptual stroke as an
  image without it, as the canvas draws it (GradientPaint::pixels, SVG's
  raster_paint and PDF's area are told whether they paint a fill).
2026-10-23 01:53:29 +07:00
bflatastic 26b9cf84ad Type: colour emoji fonts with bitmap glyphs draw and export
Squashed from #3561 onto main.
2026-10-23 00:44:31 +07:00
Taha-Mahmoodi 159e20053c Gradients: a dithered gradient without stops draws nothing, as an undithered one does
Review follow-up (CodeRabbit): a dithered fill skipped set_paint's guard for
a gradient without stops and sampled a transparent grid to draw nothing. It
now takes the same no-fill path.
2026-10-23 00:08:52 +07:00
Taha-Mahmoodi 4062e74ff3 Gradients: a dithered fill samples only the part of its grid the view shows
Review follow-up (CodeRabbit): a large dithered gradient mostly off-screen
sampled its whole grid (up to 4096 x 4096 cells, 64 MiB) to draw a small
visible part, and did it again on each zoom step. The renderer now samples
only the cells the render target can show, a cell more each way for the
bilinear mix, snapped out to 256-cell tiles so a pan within them reuses the
grid. Cells keep their place in the whole grid, so the gradient and its
noise are the same pixels as before (GradientPaint::pixels_within).
2026-10-23 00:08:52 +07:00
Taha-Mahmoodi cc1a660a6d Gradients: Dither in the Gradient panel draws a gradient fill with fine noise that hides banding
Illustrator 30.0's gradient Dither, from the public documentation
(https://helpx.adobe.com/illustrator/desktop/paint-and-fill/create-and-edit-gradients/control-dither-in-gradients.html):
a Dither checkbox in the Gradient panel; dither converts the gradient into
pixels with subtle noise, works with gradient fills only, and follows the
Document Raster Effects resolution.

- Gradient gains `dither` (serde default false, written only when on, so
  older files open unchanged and new files keep it).
- The renderer draws a dithered linear or radial fill as an image paint: the
  gradient sampled a cell per device pixel or finer (quarter-octave steps,
  cached per thread), each channel given seeded triangular noise of up to a
  level before rounding. Same pixels every run; strokes and undithered fills
  draw exactly as before. PNG and other raster exports go through it.
- SVG and PDF write a dithered fill as an image at the document's raster
  effects resolution, clipped to the shape (the freeform-gradient path);
  characters' fills stay vector gradients in SVG, with a warning.
- paint.editGradient and paint.setFill take `dither`; strokes refuse it.
  The Gradient panel's Dither box is dimmed behind the Stroke proxy.
- i18n rows in all 24 catalogs; docs: G13, target-app-parity, ui-parity,
  mcp.md, plugins.md.
2026-10-22 22:38:42 +07:00
Taha-Mahmoodi 4bb6f32911 Gradients: built with Gradient::new, so a new option touches one place 2026-10-22 22:38:41 +07:00
dotCooCoo 3a1d407f1a Place: a placed PDF or .ai can stay linked to its file (preference, off by default)
File › Place with Link kept a link only to raster images and VectorCraft
documents. A PDF, .ai or .ait page was always embedded as a group, so
Update Links never saw a newer version of the file.

A new preference, File Handling › Link Placed PDF and .ai Files
(linkPlacedPdf), is off by default. With it on, Link places the page as
a placed document, as it places a VectorCraft document: the object
keeps the file's bytes, a preview, the page number and the page's box
(crop, art, trim, bleed or media, stored as page_box). Links check,
update and relink it, Break Link and Object › Expand give the group
placing it without Link gives, every output draws the page's vectors,
and saving and reopening keep the link. With the preference off, or
with a password or textAs: "outlines", the page is embedded as before.
EPS and PostScript .ai files are still embedded.

Package copies a placed PDF or .ai file as it is. The Place dialog's
Link tooltip describes a PDF or .ai file as a document while the
preference is on. Relinking a placed document to a file that is not a
VectorCraft document, a PDF or a PDF-compatible .ai file says so.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-22 21:52:23 +07:00
dwetscherandClaude Opus 5.5 f73a3df974 AF.5: bound hostile Affinity files; fuzz the new import paths
- One budget for the whole read (layers, pixels, characters, table
  cells), shared with embedded documents and charged also when their
  reading fails; the first reading is dropped before the second.
- Tables: at most 65 536 cells; lines and cell formats that don't
  match the grid are left out instead of shifted.
- Paragraph formats matched to runs by binary search; font names
  longer than 256 bytes are dropped.
- Overlays: one work budget per import; encoded originals decoded
  within their layer's size; table cell outlines recoloured too.
- Engine: master page copies capped, each page's master found in
  linear time, line gaps looked up once per font, the working CMYK
  table built once, objects past MAX_COORD left out.
- Embedded content stays with its node; pixel masks under artistic
  text are read whole and include stroke reach; gradient overlays on
  hidden layers are kept.
- Render: contexts and raster effect images stay within
  MAX_RASTER_SIDE per side (vello overflowed near u16::MAX; found by
  the new fuzz target).
- Tests: affinity tests/import_limits.rs, feature samples in
  import_fuzz.rs, shared builders in engine/tests/support/.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 18:24:57 +07:00
bflatastic 00fe2be469 Gradient mesh: each tessellated quad shades between its corner colours, no banding 2026-10-22 17:53:20 +07:00
Jürg Lehni a2e7a2a562 Render: moved stretched type redraws where its strokes reach
Follows #3484: characters' strokes stretch with the type, past what its scale factor covers.
2026-10-22 10:26:50 +02:00
Cursor Agentandnrq6993-a11y d792e7b2be Pattern edit preview: two dimmed copy groups around centre tile
Review feedback on #3498: dimmed neighbours share one opacity composite
before the centre tile and one after, preserving overlap paint order
without per-copy compositing.

Co-authored-by: nrq6993-a11y <nrq6993-a11y@users.noreply.github.com>
2026-10-22 09:23:27 +07:00
Cursor Agentandnrq6993-a11y e87534342b Fix #3480: pattern edit preview honors Overlap stacking
## Summary
In pattern editing mode, preview copies were drawn in row-major order and the
centre tile was always composited on top, so Pattern Options Overlap had no
visible effect when negative spacing made tiles overlap (as in Illustrator).
Preview copies now paint in the same order as tiled pattern fills.

## Tests
- vectorcraft-doc: preview_paint_order_respects_overlap

Closes storytold/vectorcraft#3480

Co-authored-by: nrq6993-a11y <nrq6993-a11y@users.noreply.github.com>
2026-10-22 09:23:27 +07:00
Jürg Lehni 60f4c4f145 Render: moved type with mitred strokes redraws where its spikes reach
Follows #3461: character strokes' miter spikes reach past half the weight, which the redrawn area missed.
2026-10-22 06:52:06 +07:00
Jürg Lehni ede08590a0 Canvas: an edit redraws only the area it changed
Wide effect rasters cropped to the frame also serve parts of it.
2026-10-22 04:45:43 +07:00
bflatastic 14f804cdee Type: ruby (ふりがな) for live text
Squashed from #3448 onto main.
2026-10-22 00:10:56 +07:00
Taha-Mahmoodi 4e0cbfeede Fix #2820: images drawn small redraw from their kept mips while being moved
With image mips on (the canvas always sets them), drawing an image
fetched its full decoded pixels first, decoding them again when the
shared image store no longer held them, and only then looked up the mip
it draws. The full pixels are only peeked, so they are the least
recently used entries and the first the store drops over its budget
(1 GiB on macOS and Windows). Once a document's full-size images exceed
it (20 photos of 6000 x 4000 px are 1.9 GB as RGBA), every frame decoded
every image again, though only their small mips were drawn: moving one
image redrew seconds later.

The store now remembers the size each blob's pixels decode to, so the
mip level is found without the pixels, and a mip the store holds is
drawn (and marked used) without fetching or decoding them. Without the
mip, at full size, or with mips off (exports) drawing is unchanged.

Measured on an Apple M4 (load average 15-24 from other work), 20 noise
PNGs of 6000 x 4000 px fitted in a 2880 x 1800 view, one moved 5 pt per
frame: before, 20 decodes and 1.1-3.0 s every frame; after, the first
frame decodes 20 and the later ones none, in 3-5 ms.
2026-10-21 15:42:35 +07:00
Mirko Kerezovic d2625ec108 Fix remaining Hide Guides rendering cases (#3219)
Include guide visibility in effect raster cache inputs, skip hidden guide paths in outline members, and clear selected guide paths when hiding guides. Add regression coverage for cached effects and selection overlays.\n\nThe cached-effect regression test is adapted from the overlapping #3264 implementation.\n\nCo-authored-by: Mahtab <smmalam9@gmail.com>
2026-10-21 15:42:35 +07:00
Mirko Kerezovic 2305ebb931 Fix Show and Hide Guides (#3219) 2026-10-21 15:42:35 +07:00
HEOJUNFOandClaude Opus 5.5 5569ae97a6 Leave the pattern tile edge and swatch bounds out of exports (#3261)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 15:20:16 +07:00
bflatastic a52198cefa Appearance: image fills and strokes draw the shape Convert to Shape or Outline Object gives them
Squashed from #2500 (its merges of main dropped).
2026-10-20 02:06:54 +07:00
Hamza f106f32af4 A CMYK JPEG is not labelled JFIF
A CMYK JPEG came out with JFIF's APP0 segment at its head and four components
in the frame:

  APP0/JFIF  APP14/Adobe  APP2[ICC_PROFILE 1/4 … 4/4]  SOF0: 4 components

JFIF describes one- and three-component images, greyscale and YCbCr, so an APP0
segment in front of a four-component frame says two different things about the
file's colours: a reader that trusts JFIF decodes nonsense, and one that checks
refuses the file. Adobe's own CMYK JPEGs carry the APP14 marker the encoder
already writes, and no JFIF. `jpeg-encoder` writes JFIF unconditionally, so the
segment is replaced here.

The resolution JFIF carried moves to an EXIF APP1 segment -- `XResolution`,
`YResolution` and `ResolutionUnit` -- which is where `cmd::fileio::ppi` already
looks when a file has no JFIF, so nothing is lost; with no resolution asked for
the segment is simply dropped. RGB and greyscale files keep JFIF, which is what
it is for.

Found while looking at #3084, whose default RGB exports are valid here; this is
the file that is not.
2026-10-20 02:05:01 +07:00
Ari MarkowitzandClaude Opus 5.5 1fc3c23096 #2558: the coarser glow test also pans by fractions of a pixel
For review: after pans of (0.5, 0) and (37.3, -21.6), the Outer Glow
made coarser over its whole reach (scale 2 and 3) is still drawn from
the kept raster, placed at the nearest whole pixel as a full-size
raster kept whole is, and each frame equals a fresh renderer's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 22:19:57 +07:00
Ari MarkowitzandClaude Opus 5.5 7dc0c034d8 #2558: a coarser glow kept whole draws after a pan as made again
A test for review: an Outer Glow made coarser over its whole reach is
kept for any pan, since its grid starts at the reach's first whole
device pixel, which a whole-pixel pan moves with the art. After pans
that aren't multiples of its scale (2 and 3), the kept raster draws
exactly what a fresh renderer draws.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 22:19:57 +07:00
Ari MarkowitzandClaude Opus 5.5 be7fd07562 #2558: a drag over a wide blur draws it from the still frame's raster
An effect raster too large to keep whole (a wide blur, a shadow or glow,
or any blur zoomed in) is cropped to what can show in the view, or made
at a lower resolution and resampled to the view's pixels, and kept while
the view stays the same. Dragging other art over the object then draws
its effects from the still frame instead of previewing them at the
drag's lower resolution on every frame. An interaction's frames stay on
the render worker once it drew the frame shown, since only its renderer
holds those rasters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 22:19:57 +07:00
dotCooCoo 007f536c79 Effects: effect art redraws when fonts arrive or the color settings change
The renderer kept the art it made for an object's effects while the
object stayed the same. In the web build, type that a geometry effect
outlines, on its own, in a group or in a symbol's art, kept the
substitute font's outlines after its own font loaded. On the desktop,
Refresh Font List left them on a symbol instance whose art holds the
type. Objects with Revolve, Extrude & Bevel, Inflate or Rotate, and
objects and groups with a color adjustment, kept the colors they were
shaded or recolored with after the color settings changed. Live objects
already followed both.

The renderer now keeps that art with the inputs it keeps for the art
of live objects: the font generation when the art holds type, the art
of the symbols it shows, and the color settings. A temporary release
test timed the added key at 11 microseconds a frame for 400 type
objects with a drop shadow and 0.77 ms for 400 symbol instances of 100
paths each with a drop shadow, against warm frames of 5.3 and 72 ms.

Docs: docs/target-app-parity.md, docs/gaps.md.
2026-10-19 22:19:55 +07:00
EricKuckandClaude Fable 5.1 1b8e3d9ae1 Outer Glow grows its solid silhouette by a quarter raster pixel before the blur, as the glows AI files export measure
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-18 23:50:57 +07:00
nrq6993-a11yandCursor 16dcaa425b Fix #2727: opacity-mask editing no longer dims the canvas
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-18 23:48:57 +07:00
ham9zah bf2379f401 Part of #394: required shaping is not an alternate glyph
Highlight Alternate Glyphs (8689863f) flagged any glyph that isn't its
character's cmap glyph, so the joining forms Arabic and other complex
scripts always take (init, medi, fina, isol, rlig, ccmp and the Indic
required set) covered ordinary text in pink. A glyph is now an
alternate only when required shaping alone doesn't give it: the text is
laid out again with every optional feature off (liga, clig, calt and
the features its characters turn on) and each candidate glyph compared
by cluster. The pink boxes are measured once per text geometry, only
when the preference shows them.
2026-10-18 18:46:02 +07:00
Fabian GrajalesandClaude Opus 5.5 9735518c84 Render: a gradient mesh is rasterized into at most 4 MP (16 MB) at once
The image a mesh's quads are filled into is clipped to the frame; at 16 MP a
mesh filling a large canvas allocated up to 64 MB per frame. A larger mesh is
drawn quad by quad, backed by its patches' outlines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dt49z1ubK9x1bPq51APgz3
2026-10-18 12:41:21 +07:00
Fabian GrajalesandClaude Opus 5.5 cf5caa43d5 Render: gradient meshes laid edge to edge meet without seams
Drawing each mesh as one image through its antialiased outline (dadfa06b)
left a thin line of the background wherever two meshes met: the two
partly covered edge pixels don't add up to full coverage. A page made of
thousands of small meshes (a PDF from a gradient-mesh illustration) showed
a grid of lines on screen and in raster exports.

- The outline is flattened and moved out by half a device pixel
  (grown_outline), as the quads drawn one by one are, and filled once: no
  doubled edge, so translucent meshes are grown too.
- The edge colours spread two pixels out of the quads (the grown edge
  samples up to about a pixel and a half out), and the buffer has two
  pixels of margin.

Tests: an_outline_grows_by_half_a_pixel_both_ways_round; the edge-spread
test checks the second pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 12:41:21 +07:00
Fabian GrajalesandClaude Opus 5.5 ed69938e0e Render: gradient meshes in CMYK documents draw several times faster
A PDF of thousands of gradient meshes in a CMYK document with transparency
took minutes to export (186 s at 72 ppi for an 18,577-mesh page). Each mesh
was cut into quads coloured in RGB, and drawing a CMYK document by ink planes
separated every quad's colour back to CMYK through the profile, twice: the
4,096-colour separation cache, flooded by the meshes' interpolated colours,
cleared over and over.

- doc: GradientMesh::color_at mixes corners that are all CMYK (or all grey)
  in that space, as a PDF shading mixes in its own: exact inks, nothing to
  separate.
- render: the separation cache holds 65,536 colours.
- render: a mesh's quads are filled into one image by scanline (every pixel
  to the quad its centre is in: no seams) and drawn through the mesh's
  outline, instead of one path per quad; meshes over 16 MP on screen are
  drawn quad by quad as before, backed by each opaque patch's outline (#2456).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 12:41:21 +07:00
dotCooCoo 739b0b7335 Tests: the drag-budget test sets its thread count, and no render test changes the default
The FreeBSD job fails
fx::tests::a_drag_renders_raster_effects_within_the_drag_budget at
fx.rs:2822 with 1 previewed instead of 2.

pixels_do_not_depend_on_the_thread_count called set_default_threads
with 0, 1, 4 and 8. The override is process-wide, so a Renderer::new()
in a test running at the same time took that count. With 0 threads a
drag renders the drop shadow at full resolution, as
RenderOptions::dragging documents, so stats.previewed counts only the
filtered content (1 instead of 2).

The thread-count test now sets each renderer's threads. The drag test
gives its renderers 3 threads, so it also passes where default_threads()
is 0 (one CPU, or VECTORCRAFT_RENDER_THREADS=0).
2026-10-18 12:36:24 +07:00
dotCooCoo b27b5b80ea Place: Bounding Box measures only the art that shows
With crop "bounding", file.place measured the placed art by each
object's visual bounds, which include the hidden members of a group.
A group holding a 40 pt square and a hidden 170 x 42 pt rectangle was
measured as 251 x 123 pt: at, rect, the place cursor and
file.place.info all used that box. A group whose only part on the page
or artboard was a hidden member was also placed, and symbol instances
were measured as a 20 pt square.

The art's box now follows the rule exports use
(vectorcraft_render::encode::drawn_bounds, made public): hidden
objects, guides and template layers do not count; the reach of effects
such as a drop shadow now counts; symbol instances are measured by
their art. The same rule picks the objects on the page or artboard.
Hidden objects on the page or artboard are still placed, and stay
hidden. Replace and the width and height that file.place answers also
measure symbol instances by their art.

Docs: docs/mcp.md (Place), docs/target-app-parity.md.
2026-10-18 12:34:29 +07:00
dotCooCoo 2fc47948d5 Render: raster exports warn about an undecodable image drawn from a kept Rasterize image
When a drop shadow or an outer glow follows Effect › Rasterize, the
renderer makes the Rasterize image on the first draw and keeps it for
the shadow and the content, which draw it again. The kept image held no
count of the images that failed to decode while it was made. With a
blur or a filter after the shadow, the content's raster was made from
the kept image and recorded no failure, so a renderer that drew the
object again took the shadow and the content from its caches and
counted nothing. Pattern tiles are drawn by a renderer that keeps these
rasters across exports: a tile drawn again with the same art, for
example after Pattern Options changes the overlap, gave an export
without the "couldn't be decoded" warning although the picture lacked
the image.

The kept Rasterize image now stores how many decodes failed while it
was made, and each draw from it adds that number to the count again, as
kept effect rasters and pattern tiles do.
2026-10-18 12:34:28 +07:00
bflatastic 6e29afe648 Render: the drag budget for shadows returns None, as the cached-shadow path does since #2395 (#2745) 2026-10-18 01:27:13 +07:00
dotCooCoo aeb9314e2c Part of #2558: a drag draws zoomed-in raster effects at a lower resolution until it ends
When zoomed in, an object with a texture, a blur or a shadow was slow
to drag. Filtered content whose on-screen area passes the 8.4 million
pixel raster budget keeps no raster, and a shadow or glow larger than
4 million pixels keeps none either, so every frame of the drag ran the
effects again over up to 8.4 million pixels. A drag that scales or
rotates an object runs them again at any zoom.

The canvas now renders a tool's drag with RenderOptions::dragging. In
such a frame the renderer draws filtered content within 1,000,000
pixels, at a lower resolution where it would need more, unless it can
draw the content from an earlier frame's raster. On the multithreaded
pipeline it does the same for drop shadows, outer glows and inner
glows. No raster made that way is kept. They count as moved rasters,
so a raster made over them is made again after the drag, and the
desktop canvas renders the frame after the drag on the render worker.
That frame renders them as a still frame does. A dialog's preview,
exports and printing render as before.

zoomed_drag_perf times a drag's frames at 100% to 6400% with and
without dragging.

Docs: docs/target-app-parity.md.
2026-10-18 01:24:35 +07:00
EricKuckandClaude Opus 5.5 1f304f6a23 Shadows and glows: each is of the art with the ones before it in the stack, and an Outer Glow blurs a solid silhouette
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 01:11:39 +07:00
BFlat c1c677bc76 Merge pull request #2409 from dotCooCoo/feat/live-rasterize-effect
Effects: Effect › Rasterize draws an object as pixels while its paths stay editable
2026-10-17 20:02:59 +07:00
dotCooCoo 4712fb2c2f Character: variable fonts get a slider for each axis
VectorCraft listed a variable font's named instances as styles, but
type could not be set between or past them. The reference app's
Character panel has a Variable Font control that adjusts a variable
font's axes, such as Weight, Width and Slant.

Type now keeps Variable Font settings by axis tag
(CharStyle::font_axes, saved as fontAxes only when set). text.setFormat
and text.setRangeStyle take fontAxes: each value is clamped to its
axis's range, a tag the font has no axis for is left out, and null
goes back to the style's own setting. Another font or style clears
them, including one from a character style that does not set them. A
character style captured from type sets them, with {} for none.
text.fontList {family, style} lists a font's axes.

Layout, outlines, the substituted-glyph highlight and PDF export use
the font instanced at the settings, and neighboring runs at different
settings are shaped apart, as runs in different fonts are. SVG export
writes them as font-variation-settings, the wght setting as the weight
and the wdth setting as font-stretch, and embeds the font instanced
there; SVG import reads font-variation-settings back. Legacy .ai export
warns that the settings are left out.

The Character panel shows a Variable Font button for a variable font.
It opens a slider and a field for each axis the font does not hide,
labeled with the font's name for the axis, and screen readers get each
slider under that name. A drag is one undo step. Reset Panel leaves
the settings, as it leaves the font and style.

Missing Glyph Protection now keeps the settings and the font version
on the characters that keep their font; before, it kept only the
family and style. A character style applied or redefined with another
font now clears the old font's version, as text.setStyle does, unless
the style sets one. SVG export with embedded fonts warns when faces of
one weight, style and width share characters, which browsers then draw
in one of the faces.

Docs: docs/mcp.md, docs/target-app-parity.md, docs/type-parity.md.
2026-10-17 19:38:46 +07:00
dotCooCoo 04b0be2c9f Merge main into feat/live-rasterize-effect 2026-10-17 04:09:12 -07:00
dotCooCoo 84ccee71ef Merge main into feat/live-rasterize-effect 2026-10-17 01:25:27 -07:00
EricKuckandClaude Opus 5.5 7add248dc3 Gradient mesh: an opaque patch is backed by its outline, so patches that narrow to slivers leave no holes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 14:43:14 +07:00
dotCooCoo b9d370aeff Render: every raster export warns about an undecodable image inside a pattern
A raster export warns that "an image couldn't be decoded and is
missing from the picture" when the count of failed decodes rose while
it drew. A pattern fill draws from a tile kept for each thread, and a
tile taken from that cache draws none of its art. Only the export that
made the tile counted the image, and later exports of the document gave
no warning although their pictures lacked the image too. The raster
kept for a blurred or filtered object had the same fault: the renderer
that draws pattern tiles keeps those rasters from one tile to the next,
so a tile drawn again took such an object from its raster and counted
nothing.

A kept tile and a kept effect raster now store how many decodes failed
on their thread while they were made, and each draw from them adds that
number to the count again.
2026-10-17 13:52:06 +07:00
bflatastic 51f8582c25 MCP: a cancelled export keeps no partly drawn tile or raster, and its message is translated (#2165) 2026-10-17 13:49:48 +07:00
vangel76andClaude Opus 5.5 9122ed0a7e MCP: cancel long exports, answer while one runs, report progress (#2165, #2171, #2172)
The MCP server read one line, ran it and answered it before reading the next,
so notifications/cancelled was never seen during a call and every request
(even ping) waited behind a running export.

- vectorcraft_doc::cancel: a thread-local cancel token and progress sink that
  long work polls. Checkpoints: PDF pages, PDF flattening, geometry bake,
  raster-effect flattening, drawing objects, raster artboards, and before any
  file is written. A cancelled command returns EngineError::Cancelled and
  changes nothing.
- Server::serve reads on its own thread: notifications/cancelled drops a
  queued request or stops the running one (no reply, as the MCP spec says),
  engine-free requests (ping, the list methods, prompts/get) are answered at
  once, a request that would wait behind a call running for 2 s is refused
  with -32003 naming that call, and _meta.progressToken gets
  notifications/progress (page n of m). Queue and output are bounded.
- The remote backend stops waiting for the app when the call is cancelled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 13:49:48 +07:00
Roman Vasilyev 8b8c164e3f CI: simplify boolean guards and translate EPS mask errors 2026-10-17 13:48:06 +07:00
vangel76andClaude Opus 5.5 2108c3cc99 Render: a glow's pixmap is drawn without the paint transform a pattern fill left (#2426)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 13:20:26 +07:00
vangel76andClaude Opus 5.5 a6012d86e8 Effects: a path with its own Transform effect paints each copy with its own gradient (#2451)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 13:20:25 +07:00