Browse Source

Added Node Frames (WIP)

CPKreuz 1 year ago
parent
commit
3e995214fb

+ 16 - 0
src/PixiEditor.AvaloniaUI/Models/DocumentModels/DocumentUpdater.cs

@@ -170,6 +170,12 @@ internal class DocumentUpdater
             case DeleteNode_ChangeInfo info:
                 ProcessDeleteNode(info);
                 break;
+            case CreateNodeFrame_ChangeInfo info:
+                ProcessCreateNodeFrame(info);
+                break;
+            case DeleteNodeFrame_ChangeInfo info:
+                ProcessDeleteNodeFrame(info);
+                break;
             case ConnectProperty_ChangeInfo info:
                 ProcessConnectProperty(info);
                 break;
@@ -512,6 +518,16 @@ internal class DocumentUpdater
         doc.NodeGraphHandler.RemoveNode(info.Id);
     }
     
+    private void ProcessCreateNodeFrame(CreateNodeFrame_ChangeInfo info)
+    {
+        doc.NodeGraphHandler.AddFrame(info.Id, info.NodeIds);
+    }
+
+    private void ProcessDeleteNodeFrame(DeleteNodeFrame_ChangeInfo info)
+    {
+        doc.NodeGraphHandler.RemoveFrame(info.Id);
+    }
+
     private void ProcessConnectProperty(ConnectProperty_ChangeInfo info)
     {
         NodeViewModel outputNode = info.OutputNodeId.HasValue ? doc.StructureHelper.FindNode<NodeViewModel>(info.OutputNodeId.Value) : null;

+ 3 - 0
src/PixiEditor.AvaloniaUI/Models/Handlers/INodeGraphHandler.cs

@@ -8,11 +8,14 @@ internal interface INodeGraphHandler
 {
    public ObservableCollection<INodeHandler> AllNodes { get; }
    public ObservableCollection<NodeConnectionViewModel> Connections { get; }
+   public ObservableCollection<NodeFrameViewModel> Frames { get; }
    public INodeHandler OutputNode { get; }
    public StructureTree StructureTree { get; }
    public bool TryTraverse(Func<INodeHandler, bool> func);
    public void AddNode(INodeHandler node);
    public void RemoveNode(Guid nodeId);
+   public void AddFrame(Guid frameId, IEnumerable<Guid> nodeIds);
+   public void RemoveFrame(Guid frameId);
    public void SetConnection(NodeConnectionViewModel connection);
    public void RemoveConnection(Guid nodeId, string property);
    public void RemoveConnections(Guid nodeId);

+ 2 - 1
src/PixiEditor.AvaloniaUI/Models/Handlers/INodeHandler.cs

@@ -1,4 +1,5 @@
 using System.Collections.ObjectModel;
+using System.ComponentModel;
 using ChunkyImageLib;
 using PixiEditor.AvaloniaUI.Models.Structures;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
@@ -6,7 +7,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.AvaloniaUI.Models.Handlers;
 
-public interface INodeHandler
+public interface INodeHandler : INotifyPropertyChanged
 {
     public Guid Id { get; }
     public string NodeName { get; set; }

+ 1 - 0
src/PixiEditor.AvaloniaUI/Styles/PixiEditor.Controls.axaml

@@ -13,6 +13,7 @@
                 <MergeResourceInclude Source="avares://PixiEditor.AvaloniaUI/Styles/Templates/KeyFrame.axaml"/>
                 <MergeResourceInclude Source="avares://PixiEditor.AvaloniaUI/Styles/Templates/TimelineSlider.axaml"/>
                 <MergeResourceInclude Source="avares://PixiEditor.AvaloniaUI/Styles/Templates/TimelineGroupHeader.axaml"/>
+                <MergeResourceInclude Source="avares://PixiEditor.AvaloniaUI/Styles/Templates/NodeFrameView.axaml"/>
                 <MergeResourceInclude Source="avares://PixiEditor.AvaloniaUI/Styles/Templates/NodeGraphView.axaml"/>
                 <MergeResourceInclude Source="avares://PixiEditor.AvaloniaUI/Styles/Templates/NodeView.axaml"/>
                 <MergeResourceInclude Source="avares://PixiEditor.AvaloniaUI/Styles/Templates/NodeSocket.axaml"/>

+ 20 - 0
src/PixiEditor.AvaloniaUI/Styles/Templates/NodeFrameView.axaml

@@ -0,0 +1,20 @@
+<ResourceDictionary xmlns="https://github.com/avaloniaui"
+                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+                    xmlns:nodes="clr-namespace:PixiEditor.AvaloniaUI.Views.Nodes"
+                    xmlns:visuals="clr-namespace:PixiEditor.AvaloniaUI.Views.Visuals"
+                    xmlns:input="clr-namespace:PixiEditor.AvaloniaUI.Views.Input">
+    <ControlTheme TargetType="nodes:NodeFrameView" x:Key="{x:Type nodes:NodeFrameView}">
+        <Setter Property="Template">
+            <Setter.Value>
+                <ControlTemplate>
+                    <Grid Width="{Binding Size.X, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeFrameView}}">
+                        <Rectangle Fill="#60000000" Stroke="#90000000" StrokeThickness="2" RadiusX="10" RadiusY="10"
+                                   Width="{Binding Size.X, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeFrameView}}"
+                                   Height="{Binding Size.Y, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeFrameView}}" />
+                        <TextBlock Margin="10,10,0,0" Foreground="#C0FFFFFF" Text="Untitled..." FontStyle="Italic" />
+                    </Grid>
+                </ControlTemplate>
+            </Setter.Value>
+        </Setter>
+    </ControlTheme>
+</ResourceDictionary>

+ 35 - 0
src/PixiEditor.AvaloniaUI/Styles/Templates/NodeGraphView.axaml

@@ -87,6 +87,41 @@
                                 </DataTemplate>
                             </ItemsControl.ItemTemplate>
                         </ItemsControl>
+                    <ItemsControl
+                        ZIndex="-1"
+                        Name="PART_Frames"
+                        ItemsSource="{Binding NodeGraph.Frames, RelativeSource={RelativeSource TemplatedParent}}">
+                            <ItemsControl.ItemsPanel>
+                                <ItemsPanelTemplate>
+                                    <Canvas RenderTransformOrigin="0, 0">
+                                        <Canvas.RenderTransform>
+                                            <TransformGroup>
+                                                <ScaleTransform
+                                                    ScaleX="{Binding Scale, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
+                                                    ScaleY="{Binding Scale, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}" />
+                                                <TranslateTransform
+                                                    X="{Binding CanvasX, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
+                                                    Y="{Binding CanvasY, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}" />
+                                            </TransformGroup>
+                                        </Canvas.RenderTransform>
+                                    </Canvas>
+                                </ItemsPanelTemplate>
+                            </ItemsControl.ItemsPanel>
+                            <ItemsControl.ItemTemplate>
+                                <DataTemplate>
+                                    <nodes:NodeFrameView
+                                        TopLeft="{Binding TopLeft}"
+                                        BottomRight="{Binding BottomRight}"
+                                        Size="{Binding Size}" />
+                                </DataTemplate>
+                            </ItemsControl.ItemTemplate>
+                            <ItemsControl.ItemContainerTheme>
+                                <ControlTheme TargetType="ContentPresenter">
+                                    <Setter Property="Canvas.Left" Value="{Binding TopLeft.X}" />
+                                    <Setter Property="Canvas.Top" Value="{Binding TopLeft.Y}" />
+                                </ControlTheme>
+                            </ItemsControl.ItemContainerTheme>
+                        </ItemsControl>
                 </Grid>
             </ControlTemplate>
         </Setter>

+ 29 - 0
src/PixiEditor.AvaloniaUI/ViewModels/Document/NodeGraphViewModel.cs

@@ -5,6 +5,7 @@ using PixiEditor.AvaloniaUI.ViewModels.Nodes;
 using PixiEditor.ChangeableDocument.Actions;
 using PixiEditor.ChangeableDocument.Actions.Generated;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
 using PixiEditor.Numerics;
 
 namespace PixiEditor.AvaloniaUI.ViewModels.Document;
@@ -14,6 +15,7 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler
     public DocumentViewModel DocumentViewModel { get; }
     public ObservableCollection<INodeHandler> AllNodes { get; } = new();
     public ObservableCollection<NodeConnectionViewModel> Connections { get; } = new();
+    public ObservableCollection<NodeFrameViewModel> Frames { get; } = new();
     public StructureTree StructureTree { get; } = new();
     public INodeHandler? OutputNode { get; private set; }
 
@@ -48,6 +50,22 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler
         StructureTree.Update(this);
     }
 
+    public void AddFrame(Guid frameId, IEnumerable<Guid> nodes)
+    {
+        var frame = new NodeFrameViewModel(frameId, AllNodes.Where(x => nodes.Contains(x.Id)));
+        
+        Frames.Add(frame);
+    }
+
+    public void RemoveFrame(Guid guid)
+    {
+        var frame = Frames.FirstOrDefault(x => x.Id == guid);
+
+        if (frame == null) return;
+
+        Frames.Remove(frame);
+    }
+
     public void SetConnection(NodeConnectionViewModel connection)
     {
         var existingInputConnection = Connections.FirstOrDefault(x => x.InputProperty == connection.InputProperty);
@@ -157,6 +175,17 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler
         Internals.ActionAccumulator.AddFinishedActions(new CreateNode_Action(nodeType, Guid.NewGuid()));
     }
 
+    // TODO: Remove this
+    public void CreateNodeFrameAroundEverything()
+    {
+        CreateNodeFrame(AllNodes);
+    }
+    
+    public void CreateNodeFrame(IEnumerable<INodeHandler> nodes)
+    {
+        Internals.ActionAccumulator.AddFinishedActions(new CreateNodeFrame_Action(Guid.NewGuid(), nodes.Select(x => x.Id)));
+    }
+
     public void ConnectProperties(INodePropertyHandler? start, INodePropertyHandler? end)
     {
         if (start == null && end == null) return;

+ 115 - 0
src/PixiEditor.AvaloniaUI/ViewModels/Nodes/NodeFrameViewModel.cs

@@ -0,0 +1,115 @@
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using CommunityToolkit.Mvvm.ComponentModel;
+using PixiEditor.AvaloniaUI.Models.Handlers;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.AvaloniaUI.ViewModels.Nodes;
+
+internal class NodeFrameViewModel : ObservableObject
+{
+    private Guid id;
+    private VecD topLeft;
+    private VecD bottomRight;
+    private VecD size;
+    
+    public ObservableCollection<INodeHandler> Nodes { get; }
+
+    public Guid Id
+    {
+        get => id;
+        set => SetProperty(ref id, value);
+    }
+    
+    public VecD TopLeft
+    {
+        get => topLeft;
+        set => SetProperty(ref topLeft, value);
+    }
+
+    public VecD BottomRight
+    {
+        get => bottomRight;
+        set => SetProperty(ref bottomRight, value);
+    }
+
+    public VecD Size
+    {
+        get => size;
+        set => SetProperty(ref size, value);
+    }
+
+    public NodeFrameViewModel(Guid id, IEnumerable<INodeHandler> nodes)
+    {
+        Id = id;
+        Nodes = new ObservableCollection<INodeHandler>(nodes);
+
+        Nodes.CollectionChanged += OnCollectionChanged;
+        AddHandlers(Nodes);
+
+        CalculateBounds();
+    }
+
+    private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
+    {
+        var action = e.Action;
+        if (action != NotifyCollectionChangedAction.Add && action != NotifyCollectionChangedAction.Remove && action != NotifyCollectionChangedAction.Replace && action != NotifyCollectionChangedAction.Reset)
+        {
+            return;
+        }
+        
+        AddHandlers((IEnumerable<NodeViewModel>)e.NewItems);
+        RemoveHandlers((IEnumerable<NodeViewModel>)e.OldItems);
+    }
+
+    private void AddHandlers(IEnumerable<INodeHandler> nodes)
+    {
+        foreach (var node in nodes)
+        {
+            node.PropertyChanged += NodePropertyChanged;
+        }
+    }
+
+    private void RemoveHandlers(IEnumerable<INodeHandler> nodes)
+    {
+        foreach (var node in nodes)
+        {
+            node.PropertyChanged -= NodePropertyChanged;
+        }
+    }
+
+    private void NodePropertyChanged(object? sender, PropertyChangedEventArgs e)
+    {
+        if (e.PropertyName != nameof(INodeHandler.PositionBindable))
+        {
+            return;
+        }
+        
+        CalculateBounds();
+    }
+
+    private void CalculateBounds()
+    {
+        if (Nodes.Count == 0)
+        {
+            if (TopLeft == BottomRight)
+            {
+                BottomRight = TopLeft + new VecD(100, 100);
+            }
+            
+            return;
+        }
+        
+        var minX = Nodes.Min(n => n.PositionBindable.X) - 30;
+        var minY = Nodes.Min(n => n.PositionBindable.Y) - 45;
+        
+        var maxX = Nodes.Max(n => n.PositionBindable.X) + 130;
+        var maxY = Nodes.Max(n => n.PositionBindable.Y) + 130;
+
+        TopLeft = new VecD(minX, minY);
+        BottomRight = new VecD(maxX, maxY);
+
+        Size = BottomRight - TopLeft;
+    }
+}

+ 6 - 0
src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/NodeGraphManagerViewModel.cs

@@ -10,6 +10,12 @@ internal class NodeGraphManagerViewModel : SubViewModel<ViewModelMain>
     {
     }
 
+    [Command.Debug("PixiEditor.NodeGraph.CreateNodeFrameAroundEverything", "Create node frame", "Create node frame")]
+    public void CreateNodeFrameAroundEverything()
+    {
+        Owner.DocumentManagerSubViewModel.ActiveDocument?.NodeGraph.CreateNodeFrameAroundEverything();
+    }
+
     [Command.Internal("PixiEditor.NodeGraph.CreateNode")]
     public void CreateNode(Type nodeType)
     {

+ 33 - 0
src/PixiEditor.AvaloniaUI/Views/Nodes/NodeFrameView.cs

@@ -0,0 +1,33 @@
+using Avalonia;
+using Avalonia.Controls.Primitives;
+using PixiEditor.Numerics;
+using Point = PixiEditor.Numerics.Point;
+
+namespace PixiEditor.AvaloniaUI.Views.Nodes;
+
+public class NodeFrameView : TemplatedControl
+{
+    public static readonly StyledProperty<VecD> TopLeftProperty = AvaloniaProperty.Register<ConnectionLine, VecD>(nameof(TopLeft));
+    
+    public VecD TopLeft
+    {
+        get => GetValue(TopLeftProperty);
+        set => SetValue(TopLeftProperty, value);
+    }
+    
+    public static readonly StyledProperty<VecD> BottomRightProperty = AvaloniaProperty.Register<ConnectionLine, VecD>(nameof(BottomRight));
+    
+    public VecD BottomRight
+    {
+        get => GetValue(BottomRightProperty);
+        set => SetValue(BottomRightProperty, value);
+    }
+    
+    public static readonly StyledProperty<VecD> SizeProperty = AvaloniaProperty.Register<ConnectionLine, VecD>(nameof(Size));
+    
+    public VecD Size
+    {
+        get => GetValue(SizeProperty);
+        set => SetValue(SizeProperty, value);
+    }
+}

+ 3 - 0
src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/CreateNodeFrame_ChangeInfo.cs

@@ -0,0 +1,3 @@
+namespace PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
+
+public record CreateNodeFrame_ChangeInfo(Guid Id, IEnumerable<Guid> NodeIds) : IChangeInfo;

+ 3 - 0
src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/DeleteNodeFrame_ChangeInfo.cs

@@ -0,0 +1,3 @@
+namespace PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
+
+public record DeleteNodeFrame_ChangeInfo(Guid Id) : IChangeInfo;

+ 32 - 0
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/CreateNodeFrame_Change.cs

@@ -0,0 +1,32 @@
+using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
+
+namespace PixiEditor.ChangeableDocument.Changes.NodeGraph;
+
+internal class CreateNodeFrame_Change : Change
+{
+    private Guid id;
+    private IEnumerable<Guid> nodeIds;
+    
+    [GenerateMakeChangeAction]
+    public CreateNodeFrame_Change(Guid id, IEnumerable<Guid> nodeIds)
+    {
+        this.id = id;
+        this.nodeIds = nodeIds;
+    }
+    
+    public override bool InitializeAndValidate(Document target)
+    {
+        return true;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
+    {
+        ignoreInUndo = false;
+        return new CreateNodeFrame_ChangeInfo(id, nodeIds);
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        return new DeleteNodeFrame_ChangeInfo(id);
+    }
+}