Skip to content

Expand: pattern fills expand into their tiles clipped by the shape - #2616

Merged
bflatastic merged 1 commit into
storytold:mainfrom
dotCooCoo:feat/expand-pattern-fills
Oct 17, 2026
Merged

bflatastic merged 1 commit into
storytold:mainfrom
dotCooCoo:feat/expand-pattern-fills

Conversation

@dotCooCoo

Copy link
Copy Markdown
Contributor

What and why

Object › Expand with Fill expanded gradient fills only. A pattern fill stayed a live pattern, and object.expand.info did not report it, so the dialog's Fill option was disabled for it. In the reference app, expanding a pattern fill makes the pattern "a series of tiles that lie within that window", with the original shape as their clipping mask (community thread), and "if you use a pattern fill and you expand the fill then the pattern becomes a vector for you to edit" (community thread).

  • With Fill on, object.expand turns each visible pattern fill of a path or compound path into a clip group shaped like the object, holding the tiles the pattern lays over the object's box with fresh ids. The group isolates the tiles' blending. The tiles keep the shapes the canvas draws: a scaled pattern's stroke weights and effects scale with it (PatternDef::instances_in_with), and in a rotated, reflected, stretched or sheared pattern, or a scaled one whose art has geometry effects such as Roughen, the strokes and geometry effects of the tiles' paths become filled art (bake_appearance, as Distort Appearance uses, and Outline Stroke's conversion for brushed strokes), and their live shapes become plain paths, in their opacity masks too. Opacity masks in the tile art, unlinked ones included, move with each tile. The group takes the fill's opacity, blend mode and other effects. Geometry effects of the fill or the object, such as Offset Path, shape the group's outline and the box the tiles cover. A path in the tile art with a Transform effect set to Random is baked with its own id first, and a path with a brush that varies at random (calligraphic or scatter with Random variation, or bristle) is baked where the pattern defines it first, so every tile matches the canvas. The object's other fills and strokes stay on copies of it in paint order, and Expand is still one undo step.
  • Type with a pattern fill expands the same way after Object outlines it. object.expand.info reports pattern fills under fill, and reports gradient fills under a new gradient key as well; the dialog enables Expand Gradient To only when the selection has a gradient fill. A pattern fill stays as it is when it lays no tiles over the box, when a group or type in its art has a Transform effect set to Random, when the pattern is scaled, rotated, reflected, stretched or sheared and its art holds a symbol instance, and when the pattern is rotated, reflected, stretched or sheared, or scaled while its art has geometry effects, and its art has raster or 3D effects, or a geometry effect on a group or type.
  • Faults in the code this shares are fixed for its other users too. A fill's own effects were dropped when that fill was the object's only part (assemble); a gradient fill and, in Outline Stroke, a stroke with a gradient along or across it now keep them. Outline Stroke also gives a brushed stroke's raster effects to its brush art. A gradient fill that cannot be expanded now leaves the object as it was, instead of replacing it with a plain copy. The geometry effects of a gradient fill, and with Object off those of the object, now shape the clip group's outline, and the gradient fits it; before, they stayed on the expanded art and reshaped every strip. bake_appearance now keeps a stroke's raster effects on its filled outline and turns a gradient along or across a stroke into gradient meshes clipped to the outline, as Outline Stroke does, so Envelope Options › Distort Appearance keeps both too. PatternDef::instances_in now moves unlinked opacity masks in the tile art with each tile, for PDF, EPS, EMF and WMF export and Distort Pattern Fills too. PatternDef::offsets_covering overflowed on a region too large to count in integers, as for a pattern scaled by 1e-20; it now gives no offsets.
  • docs/mcp.md describes pattern fills under object.expand, docs/control-protocol.md describes the dialog's __gradient field, and docs/target-app-parity.md has one line for it. The change adds no user-visible strings.

How it was tested

New tests (vectorcraft-engine unless named), each failing on main:

  • tests_objexpand::a_pattern_fill_becomes_its_tiles_clipped_by_the_object: the clip group, its 12 tiles, fresh ids, a render with no pixel off by more than 2, the undo step, object.expand.info and fill: false.
  • tests_objexpand::an_expanded_pattern_fill_keeps_its_transparency_and_the_stroke_on_top: 50% Multiply on the clip group, the stroke and a fill above it in paint order, the render, and the outlined stroke with Stroke on.
  • tests_objexpand::type_filled_with_a_pattern_expands_to_outlines_holding_the_tiles: no pattern paint is left.
  • tests_objexpand::the_tiles_of_a_scaled_pattern_keep_the_stroke_weight_the_canvas_draws: a 1 pt stroke in a pattern at 200% expands to 2 pt.
  • tests_objexpand tests that render before and after Expand and compare: the_tiles_of_a_stretched_pattern_draw_their_strokes_stretched (200% × 100%), the_rounded_corners_of_a_stretched_pattern_stay_stretched, a_rounded_mask_in_a_stretched_pattern_stays_stretched, a_gradient_along_a_tile_stroke_follows_the_stroke_in_a_stretched_pattern, a_brushed_tile_stroke_draws_its_brush_stretched, a_flat_brush_in_a_rotated_pattern_turns_with_it (a quarter turn), a_pattern_inside_a_stretched_pattern_moves_with_each_tile, a_random_transform_in_the_tile_art_draws_every_tile_alike (as is and turned; a group with one stays live), a_random_brush_in_the_tile_art_draws_every_tile_alike (Confetti, as is and turned), roughen_in_the_tile_art_of_a_scaled_pattern_keeps_its_detail (200%), an_unlinked_opacity_mask_in_the_tile_art_moves_with_each_tile and tiles_that_blend_blend_only_with_each_other (a Multiply tile over a blue backdrop).
  • dialogs::expand::tests::expand_gradient_to_is_disabled_for_a_pattern_fill_alone (vectorcraft-ui-egui): Fill is enabled for a pattern fill alone and Expand Gradient To is not, until the fill is a gradient.
  • tests_objexpand::a_fill_effect_stays_on_the_expanded_fill: a drop shadow on a lone pattern fill and on a lone gradient fill.
  • tests_objexpand::a_gradient_that_cannot_be_expanded_leaves_the_object_as_it_was: a freeform gradient on a one-anchor path.
  • tests_objexpand::a_geometry_effect_shapes_the_clip_of_an_expanded_pattern_and_not_its_tiles and tests_objexpand::a_geometry_effect_shapes_the_clip_of_an_expanded_gradient_and_not_its_strips: an Offset Path on the fill and on the object, with {object: false, stroke: false}; the clip is the offset outline, no offset is left on the art, and the render matches.
  • tests_strokegradient::outline_stroke_keeps_the_effect_of_a_stroke_with_a_gradient_along_it and tests_outlinestroke::a_brushed_stroke_keeps_its_effects_on_its_brush_art.
  • bake::tests::a_baked_stroke_keeps_its_raster_effects and bake::tests::a_baked_stroke_with_a_gradient_along_it_becomes_meshes_clipped_to_its_outline (vectorcraft-effects), and pattern::tests::instances_carry_unlinked_masks_with_them and pattern::tests::a_region_too_large_to_count_has_no_offsets (vectorcraft-doc).

tests_objexpand::a_pattern_fill_that_lays_no_tiles_stays_live, tests_objexpand::a_turned_pattern_whose_tiles_have_effects_it_cannot_bake_stays_live and tests_objexpand::a_scaled_pattern_whose_tiles_hold_a_symbol_stays_live (which also expands the pattern when it only moves) check that those pattern fills are kept and the object is left as it was.

Targeted checks on the final tree: cargo clippy --all-targets -- -D warnings for vectorcraft-doc, vectorcraft-effects, vectorcraft-engine and vectorcraft-ui-egui; cargo fmt --all -- --check; cargo test -p vectorcraft-engine --lib; cargo test -p vectorcraft-effects; cargo test -p vectorcraft-doc --lib; cargo test -p vectorcraft-pdf -p vectorcraft-eps -p vectorcraft-metafile; cargo test -p vectorcraft-engine --test command_sweep; cargo test -p vectorcraft-ui-egui --lib i18n and dialogs::expand; cargo xtask brands, layers and assets.

On Windows 11 (x86-64) with rustc 1.99.1, on top of main at 8bc6532:

  • CRAFT_FONTS_DIR=<craft-fonts checkout> cargo xtask ci passes its 9 steps: 7,394 tests passed, 0 failed, 11 ignored.

git merge-tree reports no conflicts with main at 8bc6532. With #2326 it conflicts in the object.expand params text, where keeping both edits resolves it. With #1196 it conflicts in the stroke branch of bake_appearance, which both change: #1196's block, with this change's gradient-mesh case and the stroke's effects on its FillLayer added for base, resolves it. It also conflicts in the item loop of expand_fills, where keeping this change's fill_shape call and #1196's paints check resolves it.

Checklist

  • cargo xtask ci passes (fmt, clippy, tests, assets, brands, layers, wasm).
  • No panics in shipped code: no unwrap(), expect(), panic!, unreachable!, todo! or
    unimplemented! outside tests, and no unchecked indexing on data from files, parameters or
    messages. Errors are returned as Result and shown to the user
    (why and how).
  • Clean-room: nothing copied from Adobe products or GPL/AGPL code (AGENTS.md).
  • Every new asset has a row in ASSETS.md.

Object > Expand with Fill expanded gradient fills only. A pattern fill
stayed a live pattern, and object.expand.info did not report it, so the
dialog's Fill option was disabled for it. In the reference app, Expand
with Fill turns a pattern fill into the pattern's tiles, and the shape
becomes their clipping mask.

The Fill step now turns each visible pattern fill of a path or compound
path into a clip group shaped like the object, holding the tiles the
pattern lays over the object's box with fresh ids. The group isolates
the tiles' blending and takes the fill's opacity, blend mode and other
effects. Geometry effects of the fill or the object, such as Offset
Path, shape the group's outline and the box the tiles cover. A path in
the tile art with a random Transform effect is baked with its own id
first, and a path with a brush that varies at random (calligraphic or
scatter with Random variation, or bristle) is baked where the pattern
defines it first, so every tile matches the canvas. The object's other
fills and strokes stay on copies of it in paint order, and Expand stays
one undo step. Type with a pattern fill expands the same way once
Object outlines it, and object.expand.info reports pattern fills under
fill. It also reports gradient fills under a new gradient key, and the
dialog enables Expand Gradient To only when the selection has one. A
pattern fill stays as it is when it lays no tiles over the box, when a
group or type in its art has a random Transform effect, when the
pattern is scaled, rotated, reflected, stretched or sheared and its art
holds a symbol instance, and when the pattern is rotated, reflected,
stretched or sheared, or scaled while its art has geometry effects, and
its art has raster or 3D effects, or a geometry effect on a group or
type.

PatternDef gains instances_in_with, which places the tiles with a given
Scaling. Expand has stroke weights, effect distances and the tiles' own
pattern paints scale with the pattern's transform. When the pattern is
rotated, reflected, stretched or sheared, or scaled while its art has
geometry effects such as Roughen, the strokes and geometry effects of
the tiles' paths become filled art first (bake_appearance, and Outline
Stroke's conversion for brushed strokes), and their live shapes become
plain paths, in their opacity masks too. Tiles place their opacity
masks with them, unlinked ones included (place_art), which also fixes
unlinked masks in pattern tiles in PDF, EPS, EMF and WMF export and in
Distort Pattern Fills.

Faults in the shared code are fixed for every caller. assemble
dropped the effects of a sole part, so Expand lost a lone gradient
fill's own effects and Outline Stroke lost those of a stroke with a
gradient along or across it. Outline Stroke also dropped a brushed
stroke's raster effects from its brush art. Expand replaced an object
whose gradient fill could not be expanded with a plain copy; it now
leaves it as it was. Expand left the geometry effects of a gradient
fill, and with Object off those of the object, on the expanded art,
where they reshaped every strip; they now shape the clip group's
outline, and the gradient fits it, as for pattern fills.
bake_appearance dropped a stroke's raster effects from its filled
outline and filled the outline of a stroke with a gradient along or
across it with a gradient across the outline's box; it now keeps the
effects and makes the gradient meshes clipped to the outline, as
Outline Stroke does. Envelope Options > Distort Appearance uses it too.
PatternDef::offsets_covering overflowed on a region too large to count
in integers, as for a pattern scaled by 1e-20; it now gives no offsets.

Docs: docs/mcp.md, docs/control-protocol.md, docs/target-app-parity.md.
@bflatastic
bflatastic force-pushed the feat/expand-pattern-fills branch from 1f30608 to 62fd793 Compare October 17, 2026 08:48
@bflatastic
bflatastic merged commit 7b28595 into storytold:main Oct 17, 2026
@bflatastic

Copy link
Copy Markdown
Member

Thanks @dotCooCoo, merged. It met the Intertwine expand from #2622 in expand.rs; the docs and params now describe both. It went through the full gate (fmt, clippy, tests, layers, wasm) in a batch with the other PRs landed today.

dotCooCoo added a commit to dotCooCoo/vectorcraft that referenced this pull request Oct 21, 2026
outline_strokes in pathops.rs now imports reshaped from expand.rs, which
has had it since storytold#2616, and the copies of reshaped and with_path that
this branch added to pathops.rs are removed. The two copies were
identical, so Outline Stroke and Expand behave as before.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants