Skip to content

Add source metadata for SVG model commands#513

Merged
wieslawsoltes merged 6 commits into
masterfrom
fix/discussion-493-source-metadata
May 11, 2026
Merged

Add source metadata for SVG model commands#513
wieslawsoltes merged 6 commits into
masterfrom
fix/discussion-493-source-metadata

Conversation

@wieslawsoltes
Copy link
Copy Markdown
Owner

PR Summary: Source Metadata for Model Commands

Background

Discussion #493 asked whether commands generated in SKSvg.Model or RebuildFromModel() can be traced back to the SVG elements that produced them. The main use case is targeted editing, such as changing one path's fill, without reparsing the SVG document or manually matching draw commands back to source elements.

Changes

  • Added source metadata to CanvasCommand:
    • SourceElementId
    • SourceElementAddress
    • SourceElementTypeName
  • Added SKCanvas.PushCommandSource(...) so renderers can scope subsequently recorded commands to the currently rendered source node.
  • Routed command recording through an internal AddCommand(...) helper, which applies the active source metadata to commands as they are appended.
  • Preserved source metadata in CanvasCommand.DeepClone().
  • Added editing helpers for source-targeted command lookup:
    • FindCommandsBySourceElementId(...)
    • FindCommandsBySourceElementId<TCommand>(...)
    • FindCommandsBySourceElementAddress(...)
    • FindCommandsBySourceElementAddress<TCommand>(...)
  • Updated SvgSceneRenderer to push source metadata while rendering scene nodes and document background commands.
  • Backfilled source metadata for local SKPicture models generated for text and images when nested commands do not already carry source metadata.
  • Documented source-targeted model editing in README.md.

Review Fix

During review, SKCanvas.DeepClone() was adjusted so cloned canvases do not copy the active transient command-source scope. Cloned commands still preserve their existing metadata, but future commands recorded on the cloned canvas no longer inherit a stale source scope that cannot be unwound.

Tests

  • Added shim tests covering source metadata cloning, source-id lookup, source-address lookup, nested command lookup, command-source scope recording, scope restoration, clone-on-write edits, and cloned canvases created while a transient source scope is active.
  • Added Svg.Skia end-to-end tests that verify generated draw commands expose SVG source metadata and that commands for one source element id can be edited before rebuilding the native picture.

Validation

  • dotnet format Svg.Skia.slnx --no-restore --include README.md src/ShimSkiaSharp/SKCanvas.cs src/ShimSkiaSharp/Editing/SKPictureEditingExtensions.cs src/Svg.SceneGraph/SvgSceneRenderer.cs tests/ShimSkiaSharp.UnitTests/CloneCommandTests.cs tests/ShimSkiaSharp.UnitTests/EditingHelpersTests.cs tests/ShimSkiaSharp.UnitTests/SKCanvasTests.cs tests/Svg.Skia.UnitTests/SKSvgRebuildFromModelTests.cs
  • dotnet test tests/ShimSkiaSharp.UnitTests/ShimSkiaSharp.UnitTests.csproj -c Release --filter "FullyQualifiedName~SKCanvasTests|FullyQualifiedName~EditingHelpersTests|FullyQualifiedName~CloneCommandTests"
  • dotnet test tests/Svg.Skia.UnitTests/Svg.Skia.UnitTests.csproj -f net10.0 -c Release --filter "FullyQualifiedName~SKSvgRebuildFromModelTests"
  • dotnet build Svg.Skia.slnx -c Release
  • dotnet test Svg.Skia.slnx -c Release
  • git diff --check

Notes

The full build and test run completed successfully. The build still reports existing package vulnerability, nullable, and obsolete API warnings, but no errors.

@wieslawsoltes wieslawsoltes marked this pull request as ready for review May 10, 2026 19:35
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3193d50324

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

{
if (node.LocalModel is { } localModel)
{
ApplySourceMetadata(localModel, node, overwrite: false);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve source metadata for text child runs

When LocalModel is a text model, it can contain draw commands for multiple child text elements such as <tspan id="a"> and <tspan id="b">; SvgSceneTextCompiler records those runs directly from each run's StyleSource, but the recorder never pushes per-run source metadata. Backfilling the whole local model with the outer node here means FindCommandsBySourceElementId("a") returns nothing, or all commands are attributed to the parent <text> instead of the child element, which breaks the advertised element-targeted editing for common text SVGs.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 3db12bb.

This now records source metadata at the text-run level while SvgSceneTextCompiler records local text-model pictures. The compiler pushes the resolved SvgTextBase or parent anchor source around each run, using the same element-address callback as the scene compiler, so commands from child runs such as <tspan id="a"> and <tspan id="b"> carry their own SourceElementId, address, and type. The existing renderer backfill remains a fallback only (overwrite: false), so parent <text> metadata no longer replaces metadata already attached to child-run commands.

I also added regression coverage in SKSvgRebuildFromModelTests.ModelCommands_PreserveChildTextRunSourceMetadata: it checks that FindCommandsBySourceElementId("a") and "b" return the correct DrawTextCanvasCommand text, that the commands are typed as SvgTextSpan, and that text-root does not claim the child run draw commands.

Verification:

  • dotnet format Svg.Skia.slnx --no-restore --exclude externals
  • dotnet build Svg.Skia.slnx -c Release
  • dotnet test Svg.Skia.slnx -c Release

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3db12bb291

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


canvas.SaveLayer(simpleFilterPaint);
DrawPositionedTextPathRun(run, geometryBounds, ignoreAttributes, canvas, assetLoader, includeFill: true, includeStroke: true, includeDecorations: true);
DrawPositionedTextPathRun(run, geometryBounds, ignoreAttributes, canvas, assetLoader, getElementAddressKey, includeFill: true, includeStroke: true, includeDecorations: true);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Tag filter wrapper commands for text-path runs

When a filtered <textPath> run takes this branch, the surrounding Save, optional ClipRect, SaveLayer, and Restore commands are emitted outside the PushTextCommandSource scope created inside DrawPositionedTextPathRun; the complex filter branch below has the same gap around canvas.DrawPicture(sourceGraphic). This leaves the filter layer commands with null source metadata, so FindCommandsBySourceElementId cannot select or edit the full command set for that text-path element and source-targeted removals can leave orphan layer commands behind.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 5f3ba90.

The filtered text-path path now pushes the text run source metadata before emitting the filter wrapper commands. That puts the Save, optional ClipRect, SaveLayer, DrawPicture for the complex filter path, and matching Restore commands under the same source scope as the text-path run. The existing per-run source metadata inside DrawPositionedTextPathRun is still preserved for the nested glyph draw commands, but source-targeted editing now also sees the surrounding layer commands for the <textPath> element instead of leaving them untagged.

I added regression coverage for both branches:

  • ModelCommands_PreserveSimpleFilteredTextPathWrapperSourceMetadata covers the simple feGaussianBlur in="SourceGraphic" path and asserts the wrapper Save, ClipRect, SaveLayer, and Restore commands are tagged as path-text / SvgTextPath.
  • ModelCommands_PreserveComplexFilteredTextPathWrapperSourceMetadata covers the general filter path with feOffset and additionally asserts the outer DrawPicture command is tagged with the same text-path source metadata.

Verification:

  • dotnet test tests/Svg.Skia.UnitTests/Svg.Skia.UnitTests.csproj -f net10.0 -c Release --no-restore --filter "FullyQualifiedName~SKSvgRebuildFromModelTests.ModelCommands_Preserve"
  • dotnet format Svg.Skia.slnx --no-restore --exclude externals
  • dotnet build Svg.Skia.slnx -c Release
  • dotnet test Svg.Skia.slnx -c Release

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant