Commit Graph
178 Commits
Author SHA1 Message Date
Taha-Mahmoodi cab62cbb18 Fix #3964: Create Outlines, EPS, EMF/WMF and legacy .ai keep the ruby where the canvas draws it 2026-10-24 17:29:32 +07:00
Dylan M. TaylorandClaude Opus 5.5 e2a870e602 Security: bounded flattening, plug-in budgets and caches, loaded textures, Revolve paint copies, color book names and ICC names
CodeRabbit findings #4, #34, #46, #51, #77, #101, #170, #171, #188:

- `vectorcraft_geom::flatten`: kurbo's flatten with each curve's tolerance raised so it makes
  at most ~4096 segments (non-finite curves become a line); the canvas outline and
  variable-width strokes use it.
- Plug-ins: a heavy filter's budget is the host's `Limits::heavy_fuel`/`heavy_wall_time_ms`,
  never more; a start function runs on one fuel slice; the effect cache holds at most ~1M
  anchors of inputs and results, and doesn't keep a result past a quarter of that.
- Loaded textures are cached by their PNG (base64 spacing doesn't matter), and live textures
  hold at most 512 MB together.
- Revolve draws the source path when the faces' paint copies would pass 256 MB.
- Color books: names with prefix and suffix take at most 64 MB.
- ICC: localized-name (`mluc`) records over 1 MB together are refused before moxcms copies them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 17:19:54 +07:00
bflatastic 028cafa52e Scribble: the row loop's stop compares with partial_cmp (also stops on NaN; clippy) 2026-10-24 04:49:07 +07:00
Dylan M. Taylor b59235e762 Test: sliding-window maximum 2026-10-24 04:49:07 +07:00
Dylan M. Taylor e3223a419d Audit: Scribble rows counted, Outer Glow dilation in linear time, CSS export names in a set 2026-10-24 04:49:07 +07:00
bflatastic c2ad9f8bbb Type: emoji go with type that effects reshape and Expand Appearance
Squashed from #3879 onto main.
2026-10-24 04:49:06 +07:00
Taha-Mahmoodi 3b34ffb977 Pathfinder: Trap pairs only shapes within the trap's reach, not every two
Review follow-up (#3785, CodeRabbit): traps() tried every pair of visible solid paths on each redraw of the live effect and in the panel command, so many separated shapes cost quadratic work that neither the segment cap nor the overlap budget bounds. Each visible part's bounds are now computed once and a sort-and-sweep on x finds the pairs whose bounds come within the trap's reach (the clamped thickness across x, thickness × height/width across y, plus a hair for rounding); only those reach same_color, lighter and trap_band, which rejected the rest by the same bounds test, so the traps and their order are unchanged. Tests: 2,000 separated squares of two colours and one abutting pair give one candidate pair and one trap; the traps of a mixed scene match those of each pair alone, in order.
2026-10-24 03:09:08 +07:00
Taha-Mahmoodi fd345e0c56 Pathfinder: Trap spreads the lighter colour into the darker as an overprinting strip where solid colours meet
The Pathfinder panel menu's Trap… (object.pathfinder.trap) and Effect › Pathfinder › Trap… (pathfinder.trap, live on groups) share one geometry (vectorcraft_pathops::trap_band, vectorcraft_effects::trap): Thickness, Height/Width, Tint Reduction, Traps with Process Color and Reverse Traps, plus the effect's Precision and Remove Redundant Points. The panel's Trap is one undo step and groups the traps above the topmost selected object. Behaviour from https://helpx.adobe.com/illustrator/using/trapping.html and https://helpx.adobe.com/illustrator/using/pathfinder-panel-overview.html.
2026-10-24 03:09:08 +07:00
bflatastic 2e572c7805 Count Repeats inside symbol art toward the Repeat instance budget
Squashed from #3788 onto main.
2026-10-24 02:06:39 +07:00
Jürg Lehni c8fe9193d9 Type: colour glyphs go with type that effects reshape and Expand Appearance
COLR and SVG glyphs, which have no outlines, were left out wherever type was taken as its outlines: geometry effects, Expand Appearance, EMF/WMF export.
2026-10-24 02:06:11 +07:00
bflatastic c27ff0d96b Live Pathfinder budget: allow the manual lowest-bit form clippy flags, for rust-version 1.95 2026-10-23 11:08:06 +07:00
bflatastic d50afbcc1d Live Pathfinder budget: lowest set bit without the unstable isolate_lowest_one (rust-version 1.95) 2026-10-23 11:08:06 +07:00
NetterSilverandClaude Opus 5.5 c770e900e3 Cap the input of live Pathfinder effects
Fixes #3591

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 11:08:06 +07:00
Angelo Bernardini 3f16fabd89 G11 #885: Transform effect options (Transform Objects, Transform Patterns, Scale Strokes & Effects)
Squashed from #3358 onto main.
2026-10-23 06:07:37 +07:00
Brian TorresandClaude Opus 5.5 54c8a7f2bd Effects: stacked geometry effects keep to an anchor and line budget (#3578)
A chain of geometry effects makes at most 2 million anchors of a path (of a
type, symbol or image art's paths together) and about 8 million lines for the
renderer to draw at 1 px/pt, or what the path costs when that is more.
Roughen spaces its points further apart, Zig Zag makes fewer ridges a
segment, Free Distort, Twist and the warps split segments into longer pieces
(map_nonlinear_within) and Transform makes fewer copies where they would pass
it; a result drawn with too many lines is built again with fewer points, and
an effect that still doesn't fit is left out.

Three Roughens of a rectangle in a 25 KB file needed gigabytes of geometry,
and 2 million anchors of them flattened into 141 million lines (7 GB, past
vello's 2^27 lines a path); the file now opens in 0.4 s at about 450 MB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 02:44:52 +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
Taha-Mahmoodi 7c9edfaf7f Gradients: an outlined stroke leaves dither behind, and paint.setStroke refuses it
Review follow-up (CodeRabbit): paint.setStroke kept `dither: true` from its
gradient, and a stroke can hold dither anyway (swapped from a fill). Outline
Stroke and appearance expansion copied the stroke's paint into the outline's
fill, which then drew dithered although the stroke never did. The outline's
fill now goes without dither (Paint::stroke_as_fill), and paint.setStroke
refuses a gradient with `dither: true`, as paint.editGradient does.
2026-10-23 00:08:53 +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
Taha-Mahmoodi bd6d076484 Effects: Sketch › Conté Crayon draws the dark areas in a black crayon and the light areas in a white one on textured paper 2026-10-22 21:18:34 +07:00
bflatastic dd2f5da98c Expand stroke: compact plain stroke spines before outlining
Squashed from #3457 onto main.
2026-10-22 02:21:59 +07:00
Louise FoxandClaude Opus 5.5 78105cc695 Plug-ins: filters read and set effects and stroke styles (G17)
A filter could reshape and repaint objects, but not set the stroke's
join, cap or miter limit, nor the effects an object's appearance holds.
A tool that prepares objects for a live effect (a sheet of samples,
each with the effect at its own settings) had to leave the user to
apply the effect to each object by hand.

- Each input stroke has its join, cap and miterLimit beside its paint
  and width (live effects get the same, through the shared encoder),
  and each object its own effect records, {id, params, visible}.
- An output stroke may set them (miter limit clamped to 1-500), and an
  output object's "effects" replaces its own effects.
- The host admits each record as effect.apply does: an effect that
  effect.list lists or an installed live-effect plug-in, parameters it
  takes (filled in to their defaults; a plug-in's checked against its
  manifest), one 3D effect that fits the object (its fills' and
  strokes' own effects counted), and the texture of an effect with
  Load Texture (Glass, Texturizer) as the effect stores it, never a
  file to read. A record the object already had stays as
  it is, so a filter that hands its input back changes nothing. One
  refused record refuses the whole output and the document stays as
  it was.

The plug-ins crate checks the records' form; which effects exist is the
engine's to say (objects::AdmitEffects, engine plugin::admit_effects).
effects::validate_three_d is the one check of whether a 3D effect fits
an object, for effect.apply, effect.setParams and the filters.

A path handed back as it was now stays the live shape it was: write()
dropped a live rectangle's or ellipse's parameters whenever the output
held a path, changed or not, so a filter that returned its input
turned a live rectangle into a plain path.

objects/tests.rs checks the round trip, stroke styles on existing and
new strokes, the host's refusal, the bad forms refused, and a live
rectangle handed back unchanged (a plain path before the fix);
tests_plugins.rs runs filters that set a built-in and a plug-in effect,
an echo that changes nothing, and six refused outputs.
CRAFT_PLUGIN_FRAME_WASM=<folder> saves a test filter to try in the app.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhvnnnD5fTFupK8WWq35un
2026-10-22 01:25:50 +07:00
bflatastic eb9e064254 G2: Effect › Artistic › Fresco raster filter
Squashed from #3096 onto main.
2026-10-22 01:15:09 +07:00
bflatastic 5925e6339a G2: Effect › Sketch › Torn Edges raster filter
Squashed from #3010 onto main.
2026-10-22 01:06:25 +07:00
bflatastic a5c0c29b25 G2: Effect › Pathfinder › Soft Mix blends overlapping CMYK fills
Squashed from #3063 onto main.
2026-10-22 00:54:43 +07:00
bflatastic 03a2da13ba G2: Effect › Artistic › Colored Pencil raster filter
Squashed from #3028 onto main.
2026-10-22 00:30:39 +07:00
bflatastic 755f2b9158 G2: Effect › Artistic › Rough Pastels raster filter
Squashed from #3044 onto main.
2026-10-22 00:23:50 +07:00
bflatastic 695f7c0d4b G2: Effect › Artistic › Underpainting raster filter
Squashed from #3035 onto main.
2026-10-22 00:23:50 +07:00
bflatastic 03bd5e5a53 G2: Effect › Artistic › Paint Daubs raster filter
Squashed from #3032 onto main.
2026-10-22 00:23:49 +07:00
bflatastic f329f6271d G2: Effect › Artistic › Palette Knife raster filter
Squashed from #3038 onto main.
2026-10-22 00:23:48 +07:00
bflatastic 2cd7f156fb Add lighting presets to 3D effects
Squashed from #3442 onto main.
2026-10-22 00:01:37 +07:00
bflatastic c895f64063 Format: rustfmt after the batch 2026-10-21 23:38:03 +07:00
nrq6993-a11yandCursor dfa7bd12d9 G2: Texturizer Load Texture matches Glass and Illustrator.
Users can pick an image in the Texturizer dialog; it is stored as relief heights and survives save/open like Glass textures.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-21 23:38:02 +07:00
nrq6993-a11yandCursor 3f3888d040 G2: Effect › Pathfinder › Hard Mix mixes CMYK per face
Overlapping faces take the highest of each CMYK channel from every covering shape and drop strokes, matching Illustrator's Hard Mix effect.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-21 23:38:02 +07:00
bflatastic 0a3628f62f G2: Effect › Artistic › Dry Brush raster filter
Squashed from #3026 onto main.
2026-10-21 23:38:02 +07:00
dotCooCoo 8b67c4a943 Effects: expanded, flattened and exported art keeps the random Transform the canvas drew
Random in a Transform effect takes its seed from the object's id. When
art evaluated from an object repeats ids (the copies a Transform effect
makes, a copied group's members included, and the object under Crop
Marks), Expand Appearance and the baking in bake_document gave the
pieces new ids before expanding or baking their effects. The result
then drew with other random values than the canvas did: on main, 2,382
to 5,205 pixels of a 200 x 200 render changed after Expand Appearance,
and 7% to 14% after Flatten Transparency, for 50 pt squares with a
random Transform on their stroke, on their group or under Crop Marks.

bake_pieces now bakes each piece with the ids it has and gives new ids
after. expand_pieces expands the object's own pieces, and the other
members that repeat an id and hold a random Transform, before their
ids change. Expand Appearance now expands a node value
(expanded_node). The Crop Marks branch expands the object before the
object and the marks get new ids. The reply still lists the expanded
object first, then what was expanded inside it.
2026-10-21 23:14:34 +07:00
dotCooCoo d532e6d341 Use Preview Bounds: a clip group is measured where its clipping path's stroke paints
The renderer clips a clip group by its clipping path's geometry before
the path's geometry effects, and paints the path's strokes over the
clipped art without clipping them, with those effects. With Offset Path
on a stroked clipping path, the stroke paints along the offset shape
while the group was measured by the path's own stroke.

With Use Preview Bounds on, a clip group or layer whose clipping path
has strokes with geometry effects is now measured as the clipping
path's geometry and those strokes as drawn, in the Transform panel, the
bounding box, Align and Distribute. This holds inside groups and for a
clip group with a fill or stroke of its own. A clipping path
without a stroke still measures as the region it clips to, and a hidden
one adds no stroke.
2026-10-21 17:11:08 +07:00
dotCooCoo 1267efbcf9 Use Preview Bounds: objects are measured with their geometry effects
With Use Preview Bounds on, the bounding box, the Transform panel,
Align, Distribute, Distribute Spacing and object.setBounds measured
each object without its geometry effects. A square with Offset Path
10 measured as the square, and type with Outline Object measured by
its layout box instead of its glyphs.

The new vectorcraft_effects::preview module measures objects as they
are drawn. A path is measured by its effected shape, with the
geometry effects of each fill and stroke that paints. Type, images,
live objects and symbol instances are measured by their reshaped art.
A group or layer with geometry or Pathfinder effects, or with a fill
or stroke of its own that has geometry effects or paints members with
geometry effects, is measured by its evaluated art. A blend, envelope
or repeat whose objects have geometry effects is measured by its
evaluated art, with those objects measured after their effects. Raster
effects are not measured. In one object, the art of at most 4,096 symbol
instances is measured with its geometry effects, and the other
instances are measured without them. With Use Preview Bounds off, the
measures are unchanged.

object.setBounds sizes objects with geometry effects by measuring
them after each scale it tries, so type with Outline Object set to
100 pt wide gets a glyph box 100 pt wide.

Selection::anchors_box is split out of Selection::anchor_bounds. The
doc comment of VectorcraftApp::selection_bounds said that it includes
effects; it now says that it leaves them out.
2026-10-21 17:11:08 +07:00
bflatastic 15babbcacc Water Paper: no Adobe address in the shipped effect description, clippy (#2955) 2026-10-21 15:17:05 +07:00
bflatastic 55df023b65 Water Paper: rustfmt (#2955) 2026-10-21 15:17:04 +07:00
nrq6993-a11y 078e67a386 G2: Effect › Sketch › Water Paper raster filter (#2955) 2026-10-21 15:17:04 +07:00
Louise FoxandClaude Opus 5.5 2e4666a364 Plug-ins: live effects read their object's fills and strokes (G17)
A live-effect plug-in was given only its object's geometry. An effect
that draws as wide as the stroke it sits on, or tells its fills apart,
had to ask for them again as parameters, which then went stale when
the paint changed.

A live effect's input object now holds the object's fills and strokes
too, read only and in the form a filter gets them (one encoder,
objects::paint_lists, for both), and _context.item says where the
effect sits: "object", or {"fill": i} / {"stroke": i} for an effect in
a fill's or a stroke's own effects. effects::GeomContext carries the
object's appearance and the item, and the plug-in cache keys on the
paints, so a change of paint runs the plug-in again. Hosts from before
this give geometry only; nothing in the output changes.

Expand worked a fill's geometry effects out in the object's context, so
a plug-in effect there would be told "object" and a gradient fill was
expanded into a shape the canvas doesn't draw: fill_shape now gives them
the fill's context, as drawing does (expand.rs).

Siblings named with a declared prefix, the other half of such context,
are left out: the effected geometry is worked out from the object alone
wherever it is drawn, measured, hit-tested or expanded.

wat::when_given is a plug-in whose output depends on whether its input
or parameters hold a text. tests_plugins.rs uses it to check that an
effect sees the stroke's width and the fill, runs again for a thinner
stroke, and on a second stroke is told so; effect.rs checks the item
counting; a gradient fill under such an effect is drawn and expanded
into the same triangle. CRAFT_PLUGIN_PAINTS_WASM=<folder> saves a test
effect to try in the app.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhvnnnD5fTFupK8WWq35un
2026-10-20 08:39:41 +07:00
bflatastic c9d769ad4e Note Paper: grey tones on white like the other Sketch filters, rustfmt (#2954) 2026-10-20 08:30:05 +07:00
nrq6993-a11y 301783c480 G2: Effect › Sketch › Note Paper raster filter (#2954) 2026-10-20 08:28:51 +07:00
Martin Vlach f664564e97 Effects: Sketch › Bas Relief, Charcoal, Photocopy and Stamp (#2527)
Squashed from #2533 onto #2525 (Smudge Stick, Sponge, Watercolor): both sets in PIXEL_EFFECTS (45), the catalogs and the docs, whose counts now read 45/57.
2026-10-20 06:43:50 +07:00
Martin Vlach 5251246778 Effects: Artistic: Smudge Stick, Sponge and Watercolor (#2435)
Squashed from #2525 (its merges of main dropped).
2026-10-20 06:32:32 +07:00
bflatastic 8a7a1cf199 i18n: Plaster in every catalog that has the Sketch filters (#2990) 2026-10-20 04:52:37 +07:00
MarekSpodBiedry b560b97616 G2: Effect › Sketch › Plaster raster filter
Squashed from #2990 onto Chrome.
2026-10-20 04:52:26 +07:00
bflatastic 326b1c3139 i18n: Chrome in every catalog that has the Sketch filters (#2985) 2026-10-20 04:21:13 +07:00
MarekSpodBiedry 2ff683d5fe G2: Effect › Sketch › Chrome raster filter
Squashed from #2985.
2026-10-20 04:20:50 +07:00
bflatastic a5ec99d515 Effects: Graphic Pen's params doc names no vendor, as cargo xtask brands asks (#2967) 2026-10-20 03:14:27 +07:00