Browse Source

Added .pixi V4 files parsing and fixed blending/clipping rendering

flabbet 9 months ago
parent
commit
276f367124

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IClipSource.cs

@@ -4,5 +4,5 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 
 public interface IClipSource
 {
-    public void DrawOnTexture(SceneObjectRenderContext context, DrawingSurface drawOnto);
+    public void DrawClipSource(SceneObjectRenderContext context, DrawingSurface drawOnto);
 }

+ 4 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Filters.cs

@@ -38,5 +38,8 @@ public static class Filters
     public static readonly ColorFilter AverageGrayscaleFilter =
         ColorFilter.CreateColorMatrix(ColorMatrix.AverageGrayscale + ColorMatrix.OpaqueAlphaOffset);
 
-    public static ColorFilter MaskFilter => ColorFilter.CreateColorMatrix(ColorMatrix.WeightedWavelengthAlphaGrayscale);
+    /// <summary>
+    ///     R,G,B values are set to 0. Alpha is set to the average of R,G,B values.
+    /// </summary>
+    public static readonly ColorFilter MaskFilter = ColorFilter.CreateColorMatrix(ColorMatrix.WeightedWavelengthAlphaGrayscale);
 }

+ 20 - 17
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FolderNode.cs

@@ -12,12 +12,13 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 [NodeInfo("Folder")]
 public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource, IPreviewRenderable
 {
+    public const string ContentInternalName = "Content";
     private VecI documentSize;
     public RenderInputProperty Content { get; }
 
     public FolderNode()
     {
-        Content = CreateRenderInput("Content", "CONTENT");
+        Content = CreateRenderInput(ContentInternalName, "CONTENT");
     }
 
     public override Node CreateCopy() => new FolderNode { MemberName = MemberName };
@@ -103,6 +104,7 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource, IPrev
 
         AdjustPaint(useFilters);
 
+        blendPaint.BlendMode = RenderContext.GetDrawingBlendMode(BlendMode.Value);
         sceneContext.RenderSurface.Canvas.DrawSurface(outputWorkingSurface.DrawingSurface, 0, 0, blendPaint);
     }
 
@@ -187,22 +189,6 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource, IPrev
             MaskIsVisible = MaskIsVisible
         };
     }*/
-    public void DrawOnTexture(SceneObjectRenderContext context, DrawingSurface drawOnto)
-    {
-        if (Content.Connection != null)
-        {
-            var executionQueue = GraphUtils.CalculateExecutionQueue(Content.Connection.Node);
-
-            while (executionQueue.Count > 0)
-            {
-                IReadOnlyNode node = executionQueue.Dequeue();
-                if (node is IClipSource clipSource)
-                {
-                    clipSource.DrawOnTexture(context, drawOnto);
-                }
-            }
-        }
-    }
 
     public override RectD? GetPreviewBounds(int frame, string elementFor = "")
     {
@@ -239,4 +225,21 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource, IPrev
 
         return true;
     }
+
+    void IClipSource.DrawClipSource(SceneObjectRenderContext context, DrawingSurface drawOnto)
+    {
+        if (Content.Connection != null)
+        {
+            var executionQueue = GraphUtils.CalculateExecutionQueue(Content.Connection.Node);
+
+            while (executionQueue.Count > 0)
+            {
+                IReadOnlyNode node = executionQueue.Dequeue();
+                if (node is IClipSource clipSource)
+                {
+                    clipSource.DrawClipSource(context, drawOnto);
+                }
+            }
+        }
+    }
 }

+ 3 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/LayerNode.cs

@@ -67,6 +67,7 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
             ApplyRasterClip(outputWorkingSurface.DrawingSurface, tempSurface.DrawingSurface);
         }
 
+        blendPaint.BlendMode = RenderContext.GetDrawingBlendMode(BlendMode.Value);
         DrawWithResolution(outputWorkingSurface.DrawingSurface, renderOnto, context.ChunkResolution, size);
     }
 
@@ -139,8 +140,8 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
         return workingSurface;
     }
 
-    void IClipSource.DrawOnTexture(SceneObjectRenderContext context, DrawingSurface drawOnto)
+    void IClipSource.DrawClipSource(SceneObjectRenderContext context, DrawingSurface drawOnto)
     {
-        DrawLayerOnTexture(context, drawOnto, false);
+        RenderContent(GetTargetSize(context), context, drawOnto, false);
     }
 }

+ 37 - 12
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/StructureNode.cs

@@ -16,6 +16,16 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRenderInput
 {
     public const string DefaultMemberName = "DEFAULT_MEMBER_NAME";
+    public const string IsVisiblePropertyName = "IsVisible";
+    public const string OpacityPropertyName = "Opacity";
+    public const string BlendModePropertyName = "BlendMode";
+    public const string ClipToPreviousMemberPropertyName = "ClipToPreviousMember";
+    public const string MaskIsVisiblePropertyName = "MaskIsVisible";
+    public const string MaskPropertyName = "Mask";
+    public const string FiltersPropertyName = "Filters";
+    public const string FilterlessOutputPropertyName = "FilterlessOutput";
+    public const string RawOutputPropertyName = "RawOutput";
+
     public InputProperty<float> Opacity { get; }
     public InputProperty<bool> IsVisible { get; }
     public bool ClipToPreviousMember { get; set; }
@@ -37,6 +47,11 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
     {
         BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.Src, Color = Colors.Transparent
     };
+    
+    protected static readonly Paint clipPaint = new Paint()
+    {
+        BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.DstIn
+    };
 
     public virtual ShapeCorners GetTransformationCorners(KeyFrameTime frameTime)
     {
@@ -66,17 +81,17 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
         Painter rawPainter = new Painter(OnRawPaint);
 
         Background = CreateRenderInput("Background", "BACKGROUND");
-        Opacity = CreateInput<float>("Opacity", "OPACITY", 1);
-        IsVisible = CreateInput<bool>("IsVisible", "IS_VISIBLE", true);
-        BlendMode = CreateInput("BlendMode", "BLEND_MODE", Enums.BlendMode.Normal);
-        CustomMask = CreateRenderInput("Mask", "MASK");
-        MaskIsVisible = CreateInput<bool>("MaskIsVisible", "MASK_IS_VISIBLE", true);
-        Filters = CreateInput<Filter>(nameof(Filters), "FILTERS", null);
-
-        FilterlessOutput = CreateRenderOutput(nameof(FilterlessOutput), "WITHOUT_FILTERS",
+        Opacity = CreateInput<float>(OpacityPropertyName, "OPACITY", 1);
+        IsVisible = CreateInput<bool>(IsVisiblePropertyName, "IS_VISIBLE", true);
+        BlendMode = CreateInput(BlendModePropertyName, "BLEND_MODE", Enums.BlendMode.Normal);
+        CustomMask = CreateRenderInput(MaskPropertyName, "MASK");
+        MaskIsVisible = CreateInput<bool>(MaskIsVisiblePropertyName, "MASK_IS_VISIBLE", true);
+        Filters = CreateInput<Filter>(FiltersPropertyName, "FILTERS", null);
+
+        FilterlessOutput = CreateRenderOutput(FilterlessOutputPropertyName, "WITHOUT_FILTERS",
             () => filterlessPainter, () => Background.Value);
 
-        RawOutput = CreateRenderOutput(nameof(RawOutput), "RAW_LAYER_OUTPUT", () => rawPainter);
+        RawOutput = CreateRenderOutput(RawOutputPropertyName, "RAW_LAYER_OUTPUT", () => rawPainter);
 
         MemberName = DefaultMemberName;
     }
@@ -152,7 +167,7 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
                         ChunkResolution.Full,
                         surface, VecI.Zero, maskPaint);
                 }
-                else
+                else if(renderedMask != null)
                 {
                     surface.Canvas.DrawSurface(renderedMask.DrawingSurface, 0, 0, maskPaint);
                 }
@@ -211,7 +226,7 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
     {
         if (ClipToPreviousMember && Background.Value != null)
         {
-            toClip.Canvas.DrawSurface(clipSource, 0, 0, maskPaint);
+            toClip.Canvas.DrawSurface(clipSource, 0, 0, clipPaint);
         }
     }
 
@@ -228,7 +243,7 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
     protected void DrawClipSource(DrawingSurface drawOnto, IClipSource clipSource, SceneObjectRenderContext context)
     {
         blendPaint.Color = Colors.White;
-        clipSource.DrawOnTexture(context, drawOnto);
+        clipSource.DrawClipSource(context, drawOnto);
     }
 
     public abstract RectD? GetTightBounds(KeyFrameTime frameTime);
@@ -240,6 +255,10 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
         {
             additionalData["embeddedMask"] = EmbeddedMask;
         }
+        if (ClipToPreviousMember)
+        {
+            additionalData["clipToPreviousMember"] = ClipToPreviousMember;
+        }
     }
 
     internal override OneOf<None, IChangeInfo, List<IChangeInfo>> DeserializeAdditionalData(IReadOnlyDocument target,
@@ -256,6 +275,12 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
 
             return new List<IChangeInfo> { new StructureMemberMask_ChangeInfo(Id, mask != null) };
         }
+        
+        if (data.ContainsKey("clipToPreviousMember"))
+        {
+            ClipToPreviousMember = (bool)data["clipToPreviousMember"];
+            return new List<IChangeInfo> { new StructureMemberClipToMemberBelow_ChangeInfo(Id, ClipToPreviousMember) };
+        }
 
         return new None();
     }

+ 33 - 34
src/PixiEditor/Helpers/DocumentViewModelBuilder.cs

@@ -204,13 +204,13 @@ internal class AnimationDataBuilder
         FrameRate = frameRate;
         return this;
     }
-    
+
     public AnimationDataBuilder WithOnionFrames(int onionFrames)
     {
         OnionFrames = onionFrames;
         return this;
     }
-    
+
     public AnimationDataBuilder WithOnionOpacity(double onionOpacity)
     {
         OnionOpacity = onionOpacity;
@@ -276,7 +276,7 @@ internal class NodeGraphBuilder
     public NodeGraphBuilder WithOutputNode(int? toConnectNodeId, string? toConnectPropName)
     {
         var node = this.WithNodeOfType(typeof(OutputNode))
-            .WithId(AllNodes.Count + 1);
+            .WithId(AllNodes.Count);
 
         if (toConnectNodeId != null && toConnectPropName != null)
         {
@@ -291,27 +291,25 @@ internal class NodeGraphBuilder
             });
         }
 
-        AllNodes.Add(node);
         return this;
     }
 
     public NodeGraphBuilder WithImageLayerNode(string name, Surface image, out int id)
     {
-        AllNodes.Add(
-            this.WithNodeOfType(typeof(ImageLayerNode))
-                .WithName(name)
-                .WithId(AllNodes.Count + 1)
-                .WithKeyFrames(
-                [
-                    new KeyFrameData
-                    {
-                        AffectedElement = ImageLayerNode.ImageLayerKey,
-                        Data = new ChunkyImage(image),
-                        Duration = 0,
-                        StartFrame = 0,
-                        IsVisible = true
-                    }
-                ]));
+        this.WithNodeOfType(typeof(ImageLayerNode))
+            .WithName(name)
+            .WithId(AllNodes.Count)
+            .WithKeyFrames(
+            [
+                new KeyFrameData
+                {
+                    AffectedElement = ImageLayerNode.ImageLayerKey,
+                    Data = new ChunkyImage(image),
+                    Duration = 0,
+                    StartFrame = 0,
+                    IsVisible = true
+                }
+            ]);
 
         id = AllNodes.Count;
         return this;
@@ -319,21 +317,20 @@ internal class NodeGraphBuilder
 
     public NodeGraphBuilder WithImageLayerNode(string name, VecI size, out int id)
     {
-        AllNodes.Add(
-            this.WithNodeOfType(typeof(ImageLayerNode))
-                .WithName(name)
-                .WithId(AllNodes.Count + 1)
-                .WithKeyFrames(
-                [
-                    new KeyFrameData
-                    {
-                        AffectedElement = ImageLayerNode.ImageLayerKey,
-                        Data = new ChunkyImage(size),
-                        Duration = 0,
-                        StartFrame = 0,
-                        IsVisible = true
-                    }
-                ]));
+        this.WithNodeOfType(typeof(ImageLayerNode))
+            .WithName(name)
+            .WithId(AllNodes.Count)
+            .WithKeyFrames(
+            [
+                new KeyFrameData
+                {
+                    AffectedElement = ImageLayerNode.ImageLayerKey,
+                    Data = new ChunkyImage(size),
+                    Duration = 0,
+                    StartFrame = 0,
+                    IsVisible = true
+                }
+            ]);
 
         id = AllNodes.Count;
         return this;
@@ -344,6 +341,8 @@ internal class NodeGraphBuilder
         var node = new NodeBuilder();
         node.WithUniqueNodeName(nodeType.GetCustomAttribute<NodeInfoAttribute>().UniqueName);
 
+        AllNodes.Add(node);
+
         return node;
     }
 

+ 193 - 36
src/PixiEditor/Helpers/Extensions/PixiParserPixiV4DocumentEx.cs

@@ -1,11 +1,17 @@
 using System.Diagnostics.CodeAnalysis;
 using ChunkyImageLib;
 using Drawie.Backend.Core;
+using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Surfaces;
+using Drawie.Backend.Core.Surfaces.PaintImpl;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.Models.Serialization.Factories;
 using PixiEditor.Parser;
+using PixiEditor.Parser.Graph;
 using PixiEditor.Parser.Old.PixiV4;
+using PixiEditor.Parser.Skia.Encoders;
 using PixiEditor.ViewModels.Document;
 using BlendMode = PixiEditor.Parser.BlendMode;
 
@@ -15,32 +21,63 @@ internal static class PixiParserPixiV4DocumentEx
 {
     public static DocumentViewModel ToDocument(this DocumentV4 document)
     {
-        // TODO: Implement?
         return DocumentViewModel.Build(b =>
         {
-            /*b.WithSize(document.Width, document.Height)
+            b.ImageEncoderUsed = "PNG";
+            b.WithSize(document.Width, document.Height)
                 .WithPalette(document.Palette, x => new PaletteColor(x.R, x.G, x.B))
                 .WithSwatches(document.Swatches, x => new(x.R, x.G, x.B))
-                .WithReferenceLayer(document.ReferenceLayer, (r, builder) => builder
-                    .WithIsVisible(r.Enabled)
-                    .WithShape(r.Corners)
-                    .WithIsTopmost(r.Topmost)
-                    .WithSurface(Surface.Load(r.ImageBytes)));
-
-            BuildChildren(b, document.RootFolder.Children);*/
+                .WithReferenceLayer(document.ReferenceLayer, (r, builder, encoder) => builder
+                        .WithIsVisible(r.Enabled)
+                        .WithShape(r.Corners)
+                        .WithIsTopmost(r.Topmost)
+                        .WithSurface(Surface.Load(r.ImageBytes)),
+                    new PngEncoder());
+
+            b.WithGraph(graphBuilder =>
+            {
+                int lastIndex = GetIndexOfMember(document.RootFolder.Children[^1], document.RootFolder.Children);
+                graphBuilder.WithNodeOfType(typeof(OutputNode)).WithId(-1)
+                    .WithConnections([
+                        new PropertyConnection()
+                        {
+                            InputPropertyName = OutputNode.InputPropertyName,
+                            OutputNodeId = lastIndex,
+                            OutputPropertyName = "Output"
+                        }
+                    ]);
+
+                BuildChildren(graphBuilder, document.RootFolder.Children, OutputNode.InputPropertyName);
+            });
         });
 
-        void BuildChildren(ChildrenBuilder builder, IEnumerable<IStructureMember> members)
+        void BuildChildren(NodeGraphBuilder builder, IList<IStructureMember> members, string inputProperty)
         {
-            foreach (var member in members)
+            for (var i = members.Count - 1; i >= 0; i--)
             {
+                var member = members[i];
                 if (member is Folder folder)
                 {
-                    builder.WithFolder(x => BuildFolder(x, folder));
+                    builder.WithNode(nodeBuilder =>
+                    {
+                        int indexOfFolder = GetIndexOfMember(folder, document.RootFolder.Children);
+                        BuildFolder(nodeBuilder, folder, indexOfFolder);
+                        inputProperty = OutputNode.InputPropertyName;
+                    });
+
+                    if (folder.Children.Count > 0)
+                    {
+                        BuildChildren(builder, folder.Children, FolderNode.ContentInternalName);
+                    }
                 }
                 else if (member is ImageLayer layer)
                 {
-                    builder.WithLayer(x => BuildLayer(x, layer));
+                    builder.WithNode(nodeBuilder =>
+                    {
+                        int indexOfLayer = GetIndexOfMember(layer, document.RootFolder.Children);
+                        BuildLayer(nodeBuilder, layer, indexOfLayer);
+                        inputProperty = OutputNode.InputPropertyName;
+                    });
                 }
                 else
                 {
@@ -50,39 +87,159 @@ internal static class PixiParserPixiV4DocumentEx
             }
         }
 
-        void BuildFolder(FolderBuilder builder, Folder folder) => builder
-            .WithName(folder.Name)
-            .WithVisibility(folder.Enabled)
-            .WithOpacity(folder.Opacity)
-            .WithBlendMode(folder.BlendMode)
-            .WithChildren(x => BuildChildren(x, folder.Children))
-            .WithClipToBelow(folder.ClipToMemberBelow)
-            .WithMask(folder.Mask,
-                (x, m) => x.WithVisibility(m.Enabled).WithSurface(m.Width, m.Height,
-                    x => x.WithImage(m.ImageBytes, m.OffsetX, m.OffsetY)));
+        void BuildFolder(NodeGraphBuilder.NodeBuilder builder, Folder folder, int structureIndex)
+        {
+            Dictionary<string, object> inputValues = new Dictionary<string, object>();
+            inputValues["IsVisible"] = folder.Enabled;
+            inputValues["Opacity"] = folder.Opacity;
+            inputValues["BlendMode"] = (int)folder.BlendMode;
+            
+            Dictionary<string, object> additionalValues = new Dictionary<string, object>();
+            additionalValues["clipToPreviousMember"] = folder.ClipToMemberBelow;
+            
+            if (folder.Mask is not null)
+            {
+                inputValues["MaskIsVisible"] = folder.Mask.Enabled;
+                additionalValues["embeddedMask"] = new ChunkyImage(ConvertToNewMaskFormat(Surface.Load(folder.Mask.ImageBytes), document.Width, document.Height));
+            }
 
-        void BuildLayer(LayerBuilder builder, ImageLayer layer)
+            PropertyConnection contentConnection = new PropertyConnection()
+            {
+                InputPropertyName = FolderNode.ContentInternalName,
+                OutputNodeId = structureIndex - 1,
+                OutputPropertyName = "Output"
+            };
+
+            int childrenCount = CountChildren(folder.Children);
+            int previousIndex = structureIndex - childrenCount - 1;
+            PropertyConnection backgroundConnection = new PropertyConnection()
+            {
+                InputPropertyName = OutputNode.InputPropertyName,
+                OutputNodeId = previousIndex,
+                OutputPropertyName = "Output"
+            };
+            
+            builder
+                .WithId(structureIndex)
+                .WithName(folder.Name)
+                .WithUniqueNodeName("PixiEditor.Folder")
+                .WithInputValues(inputValues)
+                .WithAdditionalData(additionalValues)
+                .WithConnections([contentConnection, backgroundConnection]);
+        }
+
+        void BuildLayer(NodeGraphBuilder.NodeBuilder builder, ImageLayer layer, int structureIndex)
         {
+            Dictionary<string, object> inputValues = new Dictionary<string, object>();
+            inputValues["IsVisible"] = layer.Enabled;
+            inputValues["Opacity"] = layer.Opacity;
+            inputValues["BlendMode"] = (int)layer.BlendMode;
+
+            Dictionary<string, object> additionalValues = new Dictionary<string, object>();
+            additionalValues["clipToPreviousMember"] = layer.ClipToMemberBelow;
+
+            if (layer.Mask is not null)
+            {
+                inputValues["MaskIsVisible"] = layer.Mask.Enabled;
+                additionalValues["embeddedMask"] = new ChunkyImage(ConvertToNewMaskFormat(Surface.Load(layer.Mask.ImageBytes), document.Width, document.Height)); 
+            }
+
+            PropertyConnection connection = new PropertyConnection()
+            {
+                InputPropertyName = OutputNode.InputPropertyName, OutputNodeId = structureIndex - 1, OutputPropertyName = "Output"
+            };
+
             builder
+                .WithId(structureIndex)
                 .WithName(layer.Name)
-                .WithGuid(layer.Guid)
-                .WithVisibility(layer.Enabled)
-                .WithOpacity(layer.Opacity)
-                .WithBlendMode(layer.BlendMode)
-                .WithRect(layer.Width, layer.Height, layer.OffsetX, layer.OffsetY)
-                .WithClipToBelow(layer.ClipToMemberBelow)
-                .WithLockAlpha(layer.LockAlpha)
-                .WithMask(layer.Mask,
-                    (x, m) => x.WithVisibility(m.Enabled).WithSurface(m.Width, m.Height,
-                        x => x.WithImage(m.ImageBytes, m.OffsetX, m.OffsetY)));
-
-            if (layer is { Width: > 0, Height: > 0 })
+                .WithUniqueNodeName("PixiEditor.ImageLayer")
+                .WithInputValues(inputValues)
+                .WithAdditionalData(additionalValues)
+                .WithKeyFrames(new[]
+                {
+                    new KeyFrameData()
+                    {
+                        AffectedElement = ImageLayerNode.ImageLayerKey,
+                        Data = new ChunkyImage(Surface.Load(layer.ImageBytes)),
+                        Duration = 0,
+                        StartFrame = 0,
+                        IsVisible = true
+                    }
+                })
+                .WithConnections([connection]);
+
+            /*if (layer is { Width: > 0, Height: > 0 })
             {
                 builder.WithSurface(x => x.WithImage(layer.ImageBytes, 0, 0));
+            }*/
+        }
+    }
+    
+    private static Surface ConvertToNewMaskFormat(Surface surface, int width, int height)
+    {
+        // convert opaque pixels to white and transparent pixels to black
+        var newSurface = new Surface(new VecI(width, height));
+        newSurface.DrawingSurface.Canvas.Clear(Colors.Black);
+        using ColorFilter colorFilter = ColorFilter.CreateBlendMode(Colors.White, Drawie.Backend.Core.Surfaces.BlendMode.SrcATop);
+        using Paint paint = new()
+        {
+            Color = Colors.White,
+            BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.SrcOver,
+            ColorFilter = colorFilter
+        };
+        
+        newSurface.DrawingSurface.Canvas.DrawSurface(surface.DrawingSurface, 0, 0, paint);
+        surface.Dispose();
+        return newSurface;
+    }
+
+    private static int GetIndexOfMember(IStructureMember structureMember, IList<IStructureMember> members)
+    {
+        int index = 0;
+        bool found = GetIndexOfMember(structureMember, members, ref index);
+        return found ? index : -1;
+    }
+
+    private static bool GetIndexOfMember(IStructureMember structureMember, IList<IStructureMember> members,
+        ref int index)
+    {
+        for (int i = 0; i < members.Count; i++)
+        {
+            var member = members[i];
+
+            if (member is Folder folder1)
+            {
+                bool found = GetIndexOfMember(structureMember, folder1.Children, ref index);
+                if (found) return true;
             }
+
+            if (member == structureMember)
+            {
+                return true;
+            }
+
+            index++;
         }
+
+        return false;
     }
+    
+    private static int CountChildren(IList<IStructureMember> children)
+    {
+        int count = 0;
+        for (int i = 0; i < children.Count; i++)
+        {
+            var child = children[i];
+            if (child is Folder folder)
+            {
+                count += CountChildren(folder.Children);
+            }
 
+            count++;
+        }
+
+        return count;
+    }
 
     internal class ChildrenBuilder
     {

+ 30 - 0
src/PixiEditor/Models/DocumentModels/DocumentUpdater.cs

@@ -660,8 +660,38 @@ internal class DocumentUpdater
         NodeViewModel node = doc.StructureHelper.FindNode<NodeViewModel>(info.NodeId);
         var property = node.FindInputProperty(info.Property);
 
+        ProcessStructureMemberProperty(info, property);
+        
         property.InternalSetValue(info.Value);
     }
+    
+    private void ProcessStructureMemberProperty(PropertyValueUpdated_ChangeInfo info, INodePropertyHandler property)
+    {
+        // TODO: This most likely can be handled inside viewmodel itself
+        if (property.Node is IStructureMemberHandler structureMemberHandler)
+        {
+            if (info.Property == StructureNode.IsVisiblePropertyName)
+            {
+                structureMemberHandler.SetIsVisible((bool)info.Value);
+            }
+            else if (info.Property == StructureNode.OpacityPropertyName)
+            {
+                structureMemberHandler.SetOpacity((float)info.Value);
+            }
+            else if (info.Property == StructureNode.ClipToPreviousMemberPropertyName)
+            {
+                structureMemberHandler.SetClipToMemberBelowEnabled((bool)info.Value);
+            }
+            else if (info.Property == StructureNode.MaskIsVisiblePropertyName)
+            {
+                structureMemberHandler.SetMaskIsVisible((bool)info.Value);
+            }
+            else if (info.Property == StructureNode.BlendModePropertyName)
+            {
+                structureMemberHandler.SetBlendMode((BlendMode)info.Value);
+            }
+        }
+    }
 
     private void ProcessNodeName(NodeName_ChangeInfo info)
     {

+ 1 - 1
src/PixiEditor/ViewModels/Document/DocumentViewModel.cs

@@ -307,7 +307,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
 
         AddNodes(builderInstance.Graph);
 
-        if (builderInstance.Graph.AllNodes.Count == 0)
+        if (builderInstance.Graph.AllNodes.Count == 0 || !builderInstance.Graph.AllNodes.Any(x => x is OutputNode))
         {
             Guid outputNodeGuid = Guid.NewGuid();
             acc.AddActions(new CreateNode_Action(typeof(OutputNode), outputNodeGuid));

+ 1 - 1
src/PixiParser

@@ -1 +1 @@
-Subproject commit 8f9c8d69e6232a51025a6a91687f07b46db88543
+Subproject commit 17de4edc007ed33b062f6420c7c1d23c76e852bf