Commit Graph
192 Commits
Author SHA1 Message Date
bflatastic 5165b6eb1d Pathfinder: live compound shapes (Make, Shape Mode, Release, Expand Compound Shape)
Squashed from #4621 onto main.
2026-10-30 00:15:58 +07:00
el-j 82e4fc422c perf(effects): reorder pure-translation Transform copies after round-join terminal Offset Path (#4145)
Follow-up to #4345 / #4145 per maintainer guidance from @bflatastic.

- In `crates/effects/src/geometry.rs` & `crates/effects/src/reshape.rs`:
  - Implements `optimize_geometry_effects`: reorders pure-translation Transform effects
    with copies after a terminal round-join Offset Path effect when the effective style is safe
    (NonZero fill and no stroke).
  - Offsetting a union equals the union of offsets for round (Minkowski) offsets; moving
    pure-translation copies after the terminal offset evaluates Offset Path once on the base
    geometry rather than over dozens of multiplied copies, turning O(copies * segments) planar
    sweep explosion into a single fast offset plus lightweight affine copies.
  - Safely commutes across multiple consecutive pure-translation Transform effects.
  - Preserves exact visual geometry by leaving miter, bevel, EvenOdd, stroked, or non-terminal
    effect stacks in their original order.
- Added comprehensive unit and regression tests in `crates/effects/src/tests.rs`.
- Added contributor entry in `contributors/people.toml`.
2026-10-29 23:15:08 +07:00
Taha-Mahmoodi b41599f657 Fix #3089: a stroked line with an effect expands to a stroked line, then Expand outlines it
Squashed from #4169 onto main.
2026-10-29 18:54:59 +07:00
dotCooCoo dfa9f883c0 Fix Drop Shadow Darkness in source color space 2026-10-29 14:36:41 +07:00
Dylan M. Taylor e1bc6b235c Clarify effects dependencies and split parameter checks 2026-10-28 04:52:07 +00:00
nrq6993-a11y dfec712c2d G2: Scribble effect Settings presets
Squashed from #4274 onto main.
2026-10-27 14:36:12 +07:00
aiongg a8410bab4e refactor(effects): move lib.rs items to {catalog,geometry,params}.rs 2026-10-26 21:17:08 +00:00
aiongg f1d427762b style: cargo fmt on files moved without formatting 2026-10-26 19:32:10 +00:00
aiongg 0d282bad01 refactor(effects): move solids.rs tests to solids/tests.rs 2026-10-26 18:33:15 +00:00
aiongg 113c15c64f refactor(effects): move tests_*.rs to tests/<topic>.rs 2026-10-26 18:26:54 +00:00
aiongg e57586fdc2 refactor(effects): move group.rs tests to group/tests.rs 2026-10-26 18:18:18 +00:00
aiongg 18464c7007 refactor(effects): move bake.rs tests to bake/tests.rs 2026-10-26 18:18:17 +00:00
aiongg 6bbfb91d16 refactor(effects): move pixel/mod.rs to foo.rs files 2026-10-26 18:15:18 +00:00
Cursor Agentandnrq6993-a11y 988c958e3d ci: allow unknown_lints for manual_isolate_lowest_one on Rust 1.95
Co-authored-by: nrq6993-a11y <nrq6993-a11y@users.noreply.github.com>
2026-10-26 09:09:44 +07:00
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