瀏覽代碼

Fixed SVG export for opacity and added clipping

Krzysztof Krysiński 3 周之前
父節點
當前提交
56fd9ed13e

+ 8 - 2
src/PixiEditor.SVG/Elements/SvgGroup.cs

@@ -6,7 +6,7 @@ using PixiEditor.SVG.Units;
 namespace PixiEditor.SVG.Elements;
 
 public class SvgGroup()
-    : SvgElement("g"), ITransformable, IFillable, IStrokable, IOpacity, IElementContainer, IDefsStorage
+    : SvgElement("g"), ITransformable, IFillable, IStrokable, IOpacity, IElementContainer, IDefsStorage, IClipable
 {
     public List<SvgElement> Children { get; } = new();
     public SvgProperty<SvgTransformUnit> Transform { get; } = new("transform");
@@ -16,6 +16,8 @@ public class SvgGroup()
     public SvgProperty<SvgNumericUnit> StrokeWidth { get; } = new("stroke-width");
     public SvgProperty<SvgEnumUnit<SvgStrokeLineCap>> StrokeLineCap { get; } = new("stroke-linecap");
     public SvgProperty<SvgEnumUnit<SvgStrokeLineJoin>> StrokeLineJoin { get; } = new("stroke-linejoin");
+
+    public SvgProperty<SvgStringUnit> ClipPath { get; } = new("clip-path");
     public SvgProperty<SvgNumericUnit> Opacity { get; } = new("opacity");
     public SvgDefs Defs { get; } = new();
 
@@ -28,9 +30,13 @@ public class SvgGroup()
             Stroke,
             StrokeWidth,
             StrokeLineCap,
-            StrokeLineJoin
+            StrokeLineJoin,
+            FillOpacity,
+            ClipPath,
+            Opacity
         };
 
         ParseAttributes(properties, reader, defs); // TODO: merge with Defs?
     }
+
 }

+ 3 - 1
src/PixiEditor.SVG/Elements/SvgPrimitive.cs

@@ -5,7 +5,7 @@ using PixiEditor.SVG.Units;
 
 namespace PixiEditor.SVG.Elements;
 
-public abstract class SvgPrimitive(string tagName) : SvgElement(tagName), ITransformable, IFillable, IStrokable, IOpacity
+public abstract class SvgPrimitive(string tagName) : SvgElement(tagName), ITransformable, IFillable, IStrokable, IOpacity, IClipable
 {
     public SvgProperty<SvgTransformUnit> Transform { get; } = new("transform");
     public SvgProperty<SvgPaintServerUnit> Fill { get; } = new("fill");
@@ -18,6 +18,7 @@ public abstract class SvgPrimitive(string tagName) : SvgElement(tagName), ITrans
     public SvgProperty<SvgEnumUnit<SvgStrokeLineJoin>> StrokeLineJoin { get; } = new("stroke-linejoin");
 
     public SvgProperty<SvgNumericUnit> Opacity { get; } = new("opacity");
+    public SvgProperty<SvgStringUnit> ClipPath { get; } = new("clip-path");
 
     public override void ParseData(XmlReader reader, SvgDefs defs)
     {
@@ -31,6 +32,7 @@ public abstract class SvgPrimitive(string tagName) : SvgElement(tagName), ITrans
         properties.Add(StrokeLineCap);
         properties.Add(StrokeLineJoin);
         properties.Add(Opacity);
+        properties.Add(ClipPath);
 
         do
         {

+ 9 - 0
src/PixiEditor.SVG/Features/IClipable.cs

@@ -0,0 +1,9 @@
+using PixiEditor.SVG.Elements;
+using PixiEditor.SVG.Units;
+
+namespace PixiEditor.SVG.Features;
+
+public interface IClipable
+{
+    public SvgProperty<SvgStringUnit> ClipPath { get; }
+}

+ 18 - 0
src/PixiEditor.SVG/SvgElement.cs

@@ -116,4 +116,22 @@ public class SvgElement(string tagName)
         property.Unit ??= property.CreateDefaultUnit();
         property.Unit.ValuesFromXml(reader.Value, defs);
     }
+
+    public SvgElement Clone()
+    {
+        StringBuilder sb = new();
+        using (XmlWriter writer = XmlWriter.Create(sb, new XmlWriterSettings { OmitXmlDeclaration = true }))
+        {
+            XElement element = ToXml(XNamespace.None, new DefStorage(null));
+            element.WriteTo(writer);
+        }
+
+        using (XmlReader reader = XmlReader.Create(new StringReader(sb.ToString())))
+        {
+            reader.MoveToContent();
+            SvgElement clone = (SvgElement)Activator.CreateInstance(GetType())!;
+            clone.ParseData(reader, new SvgDefs());
+            return clone;
+        }
+    }
 }

+ 1 - 0
src/PixiEditor/Models/Handlers/IStructureMemberHandler.cs

@@ -24,6 +24,7 @@ internal interface IStructureMemberHandler : INodeHandler
     public RectD? TightBounds { get; }
     public ShapeCorners TransformationCorners { get; }
     public bool IsVisibleStructurally { get; }
+    public bool ClipToMemberBelowEnabledBindable { get; }
     public void SetMaskIsVisible(bool infoIsVisible);
     public void SetClipToMemberBelowEnabled(bool infoClipToMemberBelow);
     public void SetBlendMode(BlendMode infoBlendMode);

+ 62 - 6
src/PixiEditor/ViewModels/Document/DocumentViewModel.Serialization.cs

@@ -114,23 +114,43 @@ internal partial class DocumentViewModel
         VecD resizeFactor = new VecD(resizeFactorX, resizeFactorY);
 
         AddElements(NodeGraph.StructureTree.Members.Where(x => x.IsVisibleBindable).Reverse().ToList(), svgDocument,
-            atTime, resizeFactor, vectorExportConfig);
+            atTime, resizeFactor, vectorExportConfig, svgDocument.Defs);
 
         return svgDocument;
     }
 
     private void AddElements(IEnumerable<IStructureMemberHandler> root, IElementContainer elementContainer,
         KeyFrameTime atTime,
-        VecD resizeFactor, VectorExportConfig? vectorExportConfig)
+        VecD resizeFactor, VectorExportConfig? vectorExportConfig, SvgDefs defs)
     {
         foreach (var member in root)
         {
             if (member is FolderNodeViewModel folderNodeViewModel)
             {
-                var group = new SvgGroup();
+                var group = new SvgGroup
+                {
+                    Opacity = { Unit = new SvgNumericUnit(folderNodeViewModel.OpacityBindable, "") },
+                    Id = { Unit = new SvgStringUnit(folderNodeViewModel.NodeNameBindable) },
+                };
+
+                if (folderNodeViewModel.ClipToMemberBelowEnabledBindable &&
+                    elementContainer.Children.Count > 0)
+                {
+                    IStructureMemberHandler? previousMember =
+                        folderNodeViewModel.Inputs.FirstOrDefault(x => x.PropertyName == "Background").ConnectedOutput
+                            ?.Node as IStructureMemberHandler;
+                    var previousElement = elementContainer.Children.LastOrDefault();
+
+                    AddToClipDefs(defs, previousElement, previousMember);
+
+                    if (previousMember != null)
+                    {
+                        group.ClipPath.Unit = new SvgStringUnit($"url(#{previousMember.Id}_clip)");
+                    }
+                }
 
                 AddElements(folderNodeViewModel.Children.Where(x => x.IsVisibleBindable).Reverse().ToList(), group,
-                    atTime, resizeFactor, vectorExportConfig);
+                    atTime, resizeFactor, vectorExportConfig, defs);
                 elementContainer.Children.Add(group);
             }
 
@@ -141,13 +161,28 @@ internal partial class DocumentViewModel
             }
             else if (member is IVectorLayerHandler vectorLayerHandler)
             {
-                AddSvgShape(elementContainer, vectorLayerHandler, resizeFactor);
+                AddSvgShape(elementContainer, vectorLayerHandler, resizeFactor, defs);
             }
         }
     }
 
+    private static void AddToClipDefs(SvgDefs defs, SvgElement? previousElement, IStructureMemberHandler? previousMember)
+    {
+        if (previousElement != null)
+        {
+            var clone = previousElement.Clone();
+            (clone as SvgPrimitive).ClipPath.Unit = null;
+            clone.Id.Unit = null;
+            defs.Children.Add(new SvgClipPath()
+            {
+                Id = { Unit = new SvgStringUnit($"{previousMember.Id}_clip") },
+                Children = { previousElement }
+            });
+        }
+    }
+
     private void AddSvgShape(IElementContainer elementContainer, IVectorLayerHandler vectorLayerHandler,
-        VecD resizeFactor)
+        VecD resizeFactor, SvgDefs defs)
     {
         IReadOnlyVectorNode vectorNode =
             (IReadOnlyVectorNode)Internals.Tracker.Document.FindNode(vectorLayerHandler.Id);
@@ -183,6 +218,10 @@ internal partial class DocumentViewModel
             transform = transform.PostConcat(Matrix3X3.CreateScale((float)resizeFactor.X, (float)resizeFactor.Y));
             primitive.Transform.Unit = new SvgTransformUnit?(new SvgTransformUnit(transform));
 
+            primitive.Id.Unit = new SvgStringUnit(vectorLayerHandler.NodeNameBindable);
+
+            primitive.Opacity.Unit = new SvgNumericUnit(vectorLayerHandler.OpacityBindable, "");
+
             Paintable finalFill = data.Fill ? data.FillPaintable : new ColorPaintable(Colors.Transparent);
             primitive.Fill.Unit = new SvgPaintServerUnit(finalFill);
 
@@ -194,6 +233,23 @@ internal partial class DocumentViewModel
             primitive.Stroke.Unit = new SvgPaintServerUnit(data.Stroke);
 
             primitive.StrokeWidth.Unit = SvgNumericUnit.FromUserUnits(data.StrokeWidth);
+
+            bool clipToMemberBelowEnabled = vectorLayerHandler.ClipToMemberBelowEnabledBindable;
+            if (clipToMemberBelowEnabled && elementContainer.Children.Count > 0)
+            {
+                IStructureMemberHandler? previousMember =
+                    vectorLayerHandler.Inputs.FirstOrDefault(x => x.PropertyName == "Background").ConnectedOutput?.Node
+                        as IStructureMemberHandler;
+
+                var previousElement = elementContainer.Children[^1];
+
+                AddToClipDefs(defs, previousElement, previousMember);
+
+                if (previousMember != null)
+                {
+                    primitive.ClipPath.Unit = new SvgStringUnit($"url(#{previousMember.Id}_clip)");
+                }
+            }
         }
         else if (elementToAdd is SvgGroup group)
         {