fix(painting): build the ring without a path-ops difference

Merged
RE
2mo ago
+25-6

The bug

On the Pages gallery the beam does not hug the border — it floods the whole card face. iOS, macOS and the goldens are all correct, so this reads as a web-only rendering bug rather than a renderer quality difference.

BeamRingGeometry builds the ring by subtracting the content box from the outer shape:

Path.combine(PathOperation.difference, outer, inner)

PathOperation.difference silently returns the first operand on Flutter web (CanvasKit). So ring and halo(reach) come back as the filled shape, and every layer meant to be confined to the border — stroke, bloom, the pulse glows — paints across the entire child.

Evidence

A minimal probe, no package code, same widget tree rendered both ways:

probeSkia (test renderer)CanvasKit (web)
combine(difference, rrect, rrect)ringsolid fill
combine(difference, superellipse, superellipse)ringsolid fill
combine(difference, hand-built arc path ×2)ringsolid fill
combine(intersect, …)correctcorrect
single path, PathFillType.evenOddringring

intersect is fine, so it is difference specifically, and the path construction style is irrelevant.

The fix

A _subtract helper that expresses the ring as one even-odd path instead of a path-ops difference. For the built-in shapes inner is always simple and nested inside outer, so even-odd describes exactly the same region — no path-ops pass, identical output on every backend.

Verification

  • dart format, flutter analyze clean.
  • flutter test — all 1197 pass, every golden unchanged, so Skia/Impeller output is pixel-identical.
  • Rebuilt example for web and screenshotted it against the same gallery rendered through the Skia test renderer at identical size: they now match.

Known gap

A custom BeamPathContour can be self-intersecting (the star contour in surface_golden_test.dart is), where even-odd and non-zero disagree and the shortcut would punch a hole of its own. Those keep the path-ops route, so custom contours still render wrong on web. Fixing that needs an inverse clip via a dstOut mask layer, which costs a saveLayer and would break the budget table in save_layer_budget_test.dart — left out of this change deliberately.

No regression test is added: the bug is renderer-specific and invisible to the Skia-backed suite. Catching it would need browser tests (flutter test --platform chrome), which the repo does not set up yet.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Fixes a Flutter web rendering bug where PathOperation.difference returned the filled outer shape instead of a border ring. Built-in rings and halos now use an even-odd path, keeping strokes and glows on the border while preserving existing Skia and Impeller output.

Scope

  • Custom self-intersecting BeamPathContour values still use path subtraction and remain affected on web.
  • flutter analyze passes, and all 1,197 tests pass with unchanged goldens.