Skip to content

Add Editing Helpers#456

Merged
wieslawsoltes merged 1 commit intomasterfrom
ModelEditingHelpers
Jan 3, 2026
Merged

Add Editing Helpers#456
wieslawsoltes merged 1 commit intomasterfrom
ModelEditingHelpers

Conversation

@wieslawsoltes
Copy link
Copy Markdown
Owner

@wieslawsoltes wieslawsoltes commented Jan 3, 2026

Summary

Add editing helpers for ShimSkiaSharp and Svg.Model to make common edits (paints, paths, command filtering, drawable traversal) straightforward, with explicit in-place vs clone-on-write behavior. The work is additive and preserves existing render semantics.

API Additions

ShimSkiaSharp.Editing

  • EditMode enum with InPlace and CloneOnWrite.
  • SKPictureEditingExtensions:
    • FindCommands<TCommand>()
    • ReplaceCommands(Func<CanvasCommand, CanvasCommand?> replace)
    • UpdatePaints(predicate, update, EditMode mode = InPlace)
    • UpdatePaths(predicate, update, EditMode mode = InPlace)
  • SKPathEditingExtensions:
    • UpdateCommands(predicate, replace)
    • Transform(SKMatrix matrix) (axis-aligned only)
  • SKPaintEditingExtensions:
    • ApplyColorTransform(Func<SKColor, SKColor>)
    • ApplyShaderTransform(Func<SKShader, SKShader>)
  • Command visitor:
    • ICanvasCommandVisitor
    • CanvasCommandVisitorExtensions.Accept(this CanvasCommand, ICanvasCommandVisitor)

Svg.Model.Editing

  • DrawableWalker.Traverse overloads for drawable tree enumeration.
  • DrawableEditingExtensions:
    • UpdateFills, UpdateStrokes, UpdateOpacity with EditMode.
  • SvgDocumentEditingExtensions:
    • TraverseElements
    • UpdateStyleAttributes(predicate, update)

Implementation Details

  • UpdatePaints and UpdatePaths update each unique resource once using reference equality. In clone-on-write mode, the helpers use CloneContext to clone and preserve shared references.
  • UpdatePaths handles DrawPathCanvasCommand, DrawTextOnPathCanvasCommand, and clip paths inside ClipPathCanvasCommand, including nested ClipPath structures. In clone-on-write, predicates are evaluated on original paths and updates apply to the corresponding clones.
  • Transform rewrites PathCommand coordinates for translation/scale only. Non-uniform scale converts circles to ovals; arcs scale radii and flip sweep on reflection.
  • DrawableWalker now includes DrawablePath.MarkerDrawables and DrawableBase.MaskDrawable along with existing child containers, UseDrawable, SwitchDrawable, and MarkerDrawable edges.
  • DrawableEditingExtensions clone paints once per shared reference and reuse clones across drawables.
  • SvgDocumentEditingExtensions traverses the SVG DOM (including the document root) and updates matching SvgVisualElement instances.

Tests

New unit tests cover:

  • Command filtering and replacement.
  • Clone-on-write paint/path updates with shared references.
  • Clip-path traversal and updates.
  • Path transform behavior, including reflection of arcs.
  • Drawable traversal including marker drawables.
  • SVG DOM traversal and attribute updates.

Tests are in:

  • tests/ShimSkiaSharp.UnitTests/EditingHelpersTests.cs
  • tests/Svg.Model.UnitTests/EditingHelpersTests.cs

Compatibility

No breaking changes. All additions are new helpers and do not change existing rendering behavior.

@wieslawsoltes wieslawsoltes merged commit e6f2c7a into master Jan 3, 2026
14 checks passed
@wieslawsoltes
Copy link
Copy Markdown
Owner Author

Editing Helpers

Overview

This feature adds small, focused editing helpers to ShimSkiaSharp and Svg.Model. The goal is to make common edits (paint updates, path edits, command filtering, drawable traversal) easy and safe while keeping rendering behavior unchanged. All APIs are additive.

Namespaces and Types

ShimSkiaSharp.Editing

EditMode

public enum EditMode
{
    InPlace,
    CloneOnWrite
}
  • InPlace updates the existing objects.
  • CloneOnWrite clones shared objects before mutation to avoid cross-reference side effects. Shared references stay shared, but on the clone side.

SKPictureEditingExtensions

public static class SKPictureEditingExtensions
{
    public static IEnumerable<TCommand> FindCommands<TCommand>(this SKPicture picture)
        where TCommand : CanvasCommand;

    public static int ReplaceCommands(
        this SKPicture picture,
        Func<CanvasCommand, CanvasCommand?> replace);

    public static int UpdatePaints(
        this SKPicture picture,
        Func<SKPaint, bool> predicate,
        Action<SKPaint> update,
        EditMode mode = EditMode.InPlace);

    public static int UpdatePaths(
        this SKPicture picture,
        Func<SKPath, bool> predicate,
        Action<SKPath> update,
        EditMode mode = EditMode.InPlace);
}

Behavior notes:

  • FindCommands<T> returns a filtered view of SKPicture.Commands. Returns an empty sequence when commands are null.
  • ReplaceCommands replaces or removes commands. Returning null removes the command. The return value is the count of replacements/removals.
  • UpdatePaints updates each unique paint once (by reference), even if shared across commands. In clone-on-write mode, paints are cloned and updates apply to the clone.
  • UpdatePaths updates each unique path once and also traverses ClipPathCanvasCommand clip paths, including nested ClipPath structures. In clone-on-write mode, it clones the clip path tree and applies updates to the clone while matching predicates against the original paths.

SKPathEditingExtensions

public static class SKPathEditingExtensions
{
    public static int UpdateCommands(
        this SKPath path,
        Func<PathCommand, bool> predicate,
        Func<PathCommand, PathCommand> replace);

    public static void Transform(this SKPath path, SKMatrix matrix);
}

Behavior notes:

  • UpdateCommands replaces matching commands. It does not remove commands; it only replaces them. The return value is the number of matches.
  • Transform supports axis-aligned transforms (translation and scale). Rotation, skew, or perspective throw NotSupportedException.
  • For non-uniform scale, circles become ovals. Arc radii are scaled and arc sweep flips when the matrix reflects (negative determinant).

SKPaintEditingExtensions

public static class SKPaintEditingExtensions
{
    public static void ApplyColorTransform(this SKPaint paint, Func<SKColor, SKColor> transform);
    public static void ApplyShaderTransform(this SKPaint paint, Func<SKShader, SKShader> transform);
}

Behavior notes:

  • Both methods are null-safe for Color and Shader and throw on null arguments.

Canvas command visitor

public interface ICanvasCommandVisitor
{
    void Visit(ClipPathCanvasCommand cmd);
    void Visit(ClipRectCanvasCommand cmd);
    void Visit(DrawImageCanvasCommand cmd);
    void Visit(DrawPathCanvasCommand cmd);
    void Visit(DrawTextBlobCanvasCommand cmd);
    void Visit(DrawTextCanvasCommand cmd);
    void Visit(DrawTextOnPathCanvasCommand cmd);
    void Visit(RestoreCanvasCommand cmd);
    void Visit(SaveCanvasCommand cmd);
    void Visit(SaveLayerCanvasCommand cmd);
    void Visit(SetMatrixCanvasCommand cmd);
}

public static class CanvasCommandVisitorExtensions
{
    public static void Accept(this CanvasCommand command, ICanvasCommandVisitor visitor);
}

Behavior notes:

  • Accept throws NotSupportedException for unknown command types.

Svg.Model.Editing

DrawableWalker

public static class DrawableWalker
{
    public static IEnumerable<DrawableBase> Traverse(DrawableBase root);
    public static IEnumerable<DrawableBase> Traverse(IEnumerable<DrawableBase> roots);
}

Behavior notes:

  • Pre-order traversal using a stack and reference-based visited set to avoid cycles.
  • Walks DrawableContainer.ChildrenDrawables, DrawablePath.MarkerDrawables, UseDrawable.ReferencedDrawable, SwitchDrawable.FirstChild, MarkerDrawable.MarkerElementDrawable, and DrawableBase.MaskDrawable.

DrawableEditingExtensions

public static class DrawableEditingExtensions
{
    public static int UpdateFills(
        this DrawableBase root,
        Func<SKPaint, bool> predicate,
        Action<SKPaint> update,
        EditMode mode = EditMode.InPlace);

    public static int UpdateStrokes(
        this DrawableBase root,
        Func<SKPaint, bool> predicate,
        Action<SKPaint> update,
        EditMode mode = EditMode.InPlace);

    public static int UpdateOpacity(
        this DrawableBase root,
        Func<SKPaint, bool> predicate,
        Action<SKPaint> update,
        EditMode mode = EditMode.InPlace);
}

Behavior notes:

  • Updates each unique paint once. The return value is the number of unique paints updated.
  • In clone-on-write mode, shared paints are cloned once and reused across drawables that referenced the original paint.

SvgDocumentEditingExtensions

public static class SvgDocumentEditingExtensions
{
    public static IEnumerable<SvgElement> TraverseElements(this SvgDocument document);
    public static int UpdateStyleAttributes(
        this SvgDocument document,
        Func<SvgVisualElement, bool> predicate,
        Action<SvgVisualElement> update);
}

Behavior notes:

  • TraverseElements returns the document itself plus all descendant elements, pre-order.
  • UpdateStyleAttributes updates matching SvgVisualElement instances and returns the count.

Examples

Update paints in a picture (clone-on-write)

using ShimSkiaSharp.Editing;

picture.UpdatePaints(
    paint => paint.Color is { },
    paint =>
    {
        var c = paint.Color!.Value;
        paint.Color = new SKColor(c.Red, c.Red, c.Red, c.Alpha);
    },
    EditMode.CloneOnWrite);

Update paths inside commands and clip paths

using ShimSkiaSharp.Editing;

picture.UpdatePaths(
    path => path.Commands is { Count: > 0 },
    path => path.LineTo(10, 10),
    EditMode.CloneOnWrite);

Update strokes on drawables

using Svg.Model.Editing;

root.UpdateStrokes(
    paint => true,
    paint => paint.StrokeWidth *= 2f,
    EditMode.CloneOnWrite);

Update SVG DOM attributes

using Svg.Model.Editing;

document.UpdateStyleAttributes(
    element => element.ID == "target",
    element => element.Visibility = "hidden");

Tests

Coverage is provided in:

  • tests/ShimSkiaSharp.UnitTests/EditingHelpersTests.cs
  • tests/Svg.Model.UnitTests/EditingHelpersTests.cs

The tests cover command replacement, clone-on-write behavior, clip-path traversal, path transforms (including sweep flips for reflections), drawable traversal (including markers), and SVG DOM updates.

Compatibility and Limitations

  • All APIs are additive and do not change existing rendering or serialization behavior.
  • Transform only supports axis-aligned matrices (translation and scale). Rotation, skew, and perspective are not supported.
  • UpdateCommands does not remove commands; it only replaces matches.

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