fix(painting): build the ring without a path-ops difference
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:
| probe | Skia (test renderer) | CanvasKit (web) |
|---|---|---|
combine(difference, rrect, rrect) | ring | solid fill |
combine(difference, superellipse, superellipse) | ring | solid fill |
combine(difference, hand-built arc path ×2) | ring | solid fill |
combine(intersect, …) | correct | correct |
single path, PathFillType.evenOdd | ring | ring |
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 analyzeclean.flutter test— all 1197 pass, every golden unchanged, so Skia/Impeller output is pixel-identical.- Rebuilt
examplefor 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
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
BeamPathContourvalues still use path subtraction and remain affected on web. flutter analyzepasses, and all 1,197 tests pass with unchanged goldens.