Skip to content

Support clip-path in SF Symbol output#124

Open
rursache wants to merge 1 commit intoswhitty:mainfrom
rursache:feat/sfsymbol-clip-path
Open

Support clip-path in SF Symbol output#124
rursache wants to merge 1 commit intoswhitty:mainfrom
rursache:feat/sfsymbol-clip-path

Conversation

@rursache
Copy link
Copy Markdown
Contributor

Summary

Fixes #37

Pre-change, emitting an sfsymbol for an SVG that used <clipPath> printed clip-path unsupported in SF Symbols and produced no content. This PR bakes path ∩ clipPath into the emitted symbol path so the final SVG contains the already-clipped geometry (SF Symbols themselves still don't support clipPath, so the clip has to be resolved at generation time).

Approach

  • Follows the existing Renderer.SFSymbol+CGPath.swift precedent: uses CoreGraphics from inside the SFSymbol renderer when it gives us hard geometry we'd otherwise have to implement from scratch, all behind #if canImport(CoreGraphics)
  • New Renderer.SFSymbol+ClipPath.swift uses CGPath.intersection(_:using:) on macOS 13 / iOS 16 / tvOS 16 / watchOS 9 with a bbox-only fallback for older deployment targets so nothing crashes or silently corrupts output
  • LayerTree.Layer.clip is no longer an early-reject in getSymbolPaths. After the recursion collects child paths the new applyClip runs each through the intersector. Entirely-outside shapes drop, entirely-inside shapes pass through, partial intersections become baked geometry
  • Honors clip-rule (evenodd/nonzero) and per-child transform inside <clipPath>
  • Adds clipPathUnits (userSpaceOnUse default + objectBoundingBox) by threading a new DOM.ClipPath.Units through the parser and a new LayerTree.ClipUnits on LayerTree.Layer. The SFSymbol clipper applies translate(bbox.x, bbox.y) * scale(bbox.w, bbox.h) when units are objectBoundingBox, computed against the path being clipped

What's covered

  • <clipPath> with rect, circle, ellipse, polygon, path children
  • Multiple children unioned inside a single clipPath
  • clip-path on a <g> propagating to all descendants
  • clip-rule="evenodd" producing hollowed regions
  • clipPathUnits="objectBoundingBox" with shape-relative coordinates
  • Per-child transform inside <clipPath>
  • Composition with transform on the clipped element's parent
  • Identity pass-through when the clip fully contains the shape
  • Drop when the clip is entirely outside the shape

Tests

  • 12 new SVG fixtures under SwiftDraw/Tests/Test.bundle/clip-*.svg covering every variant above
  • New RendererSFSymbolClipPathTests with 18 cases:
    • The exact issue Missing support for clipPath #37 minimal SVG embedded inline, asserting non-empty paths in all three weight variants
    • One geometry test per fixture, comparing the intersected path.bounds against the analytically expected result with 0.5pt tolerance to absorb bezier approximation
    • Two LayerTree.Builder tests confirming clipUnits flows through from DOM for both default and objectBoundingBox
    • Full-pipeline tests through the SF Symbol template (counts paths per weight variant, asserts empty-result throws No valid content)

swift test: 214 passed, 0 failed (was 196 pre-fix, +18 new). No regressions in the pre-existing rasterization or sfsymbol tests.

Test plan

  • swift test passes across the full package
  • Pre-fix repro SVG (100x100 rect clipped by r=30 circle) now renders a symbol whose paths match the clipping circle
  • Existing PNG rasterization of a clipPath-free SVG (Moonsama.svg linked in Missing support for clipPath #37) is unchanged
  • Existing sfsymbol output for SVGs without clipPath is unchanged (chart.svg, dash.svg, key.svg tests all still pass)

Removes the early-return that printed "clip-path unsupported in SF Symbols"
and instead bakes path ∩ clipPath into the emitted symbol path using
CGPath.intersection on macOS 13/iOS 16+. Mirrors the existing
Renderer.SFSymbol+CGPath.swift pattern used for stroke outlines.

Adds clipPathUnits support (userSpaceOnUse + objectBoundingBox) by
threading a new LayerTree.ClipUnits through DOM.ClipPath and Layer.

Fixes swhitty#37
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 22, 2026

Codecov Report

❌ Patch coverage is 95.49839% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.95%. Comparing base (8f03233) to head (54348dd).

Files with missing lines Patch % Lines
.../Sources/Renderer/Renderer.SFSymbol+ClipPath.swift 81.35% 11 Missing ⚠️
...ests/Renderer/Renderer.SFSymbolClipPathTests.swift 99.06% 2 Missing ⚠️
SwiftDraw/Sources/LayerTree/LayerTree.Layer.swift 66.66% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #124      +/-   ##
==========================================
+ Coverage   89.80%   89.95%   +0.14%     
==========================================
  Files         159      161       +2     
  Lines       15868    16174     +306     
==========================================
+ Hits        14251    14549     +298     
- Misses       1617     1625       +8     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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.

Missing support for clipPath

1 participant