├── .github └── workflows │ └── release-packages.yml ├── .gitignore ├── LICENSE.md ├── NodeEditor.sln ├── Qkmaxware.Blazor.NodeEditor ├── Components │ ├── BasePropertyDrawer.cs │ ├── FieldDrawer.razor │ ├── FieldDrawer.razor.css │ ├── ProcessGraphEditor.razor │ └── ProcessGraphEditor.razor.css ├── Qkmaxware.Blazor.NodeEditor.csproj ├── _Imports.razor ├── data │ ├── CustomPropertyDrawer.cs │ ├── Graph.cs │ ├── NodeGenerator.cs │ ├── Nodes.cs │ ├── ProcessGraph.cs │ └── ProcessStep.cs └── wwwroot │ └── .gitkeep ├── Readme.md ├── Test ├── App.razor ├── Data │ └── NodeTypes.cs ├── Pages │ ├── Error.cshtml │ ├── Error.cshtml.cs │ ├── Index.razor │ ├── _Host.cshtml │ └── index.razor.css ├── Program.cs ├── Properties │ └── launchSettings.json ├── Shared │ ├── MainLayout.razor │ └── OutputDrawer.razor ├── Startup.cs ├── Test.csproj ├── _Imports.razor ├── appsettings.Development.json ├── appsettings.json ├── preview.png └── wwwroot │ ├── css │ ├── bootstrap │ │ ├── bootstrap.min.css │ │ └── bootstrap.min.css.map │ ├── open-iconic │ │ ├── FONT-LICENSE │ │ ├── ICON-LICENSE │ │ ├── README.md │ │ └── font │ │ │ ├── css │ │ │ └── open-iconic-bootstrap.min.css │ │ │ └── fonts │ │ │ ├── open-iconic.eot │ │ │ ├── open-iconic.otf │ │ │ ├── open-iconic.svg │ │ │ ├── open-iconic.ttf │ │ │ └── open-iconic.woff │ └── site.css │ └── favicon.ico └── nuget.config /.github/workflows/release-packages.yml: -------------------------------------------------------------------------------- 1 | name: Publish Packages 2 | on: 3 | release: 4 | types: [ published ] 5 | jobs: 6 | publish: 7 | name: Publish 8 | runs-on: ${{ matrix.os }} 9 | permissions: 10 | contents: read 11 | packages: write 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | os: [ ubuntu-latest ] 16 | dotnet: [ '5.0.x' ] 17 | steps: 18 | - name: Checkout Code 19 | uses: actions/checkout@master 20 | - name: Setup .NET Core - ${{ matrix.dotnet }}@${{ matrix.os }} 21 | uses: actions/setup-dotnet@v1 22 | with: 23 | dotnet-version: '${{ matrix.dotnet }}' 24 | - name: Restore Packages 25 | env: 26 | GITHUB_USERNAME: qkmaxware 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | run: dotnet restore --configfile nuget.config 29 | - name: Pack Nuget Package 30 | env: 31 | # Work around https://github.com/actions/setup-dotnet/issues/29 32 | DOTNET_ROOT: ${{ runner.tool_cache }}/dncs/${{ matrix.dotnet }}/x64 33 | run: dotnet pack --configuration Release --output . 34 | - name: Publish Nuget Package 35 | env: 36 | # Work around https://github.com/actions/setup-dotnet/issues/29 37 | DOTNET_ROOT: ${{ runner.tool_cache }}/dncs/${{ matrix.dotnet }}/x64 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | run: dotnet nuget push "*.nupkg" --source "qkmaxware" --skip-duplicate --api-key ${GITHUB_TOKEN} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | *.nupkg -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Colin Halseth 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /NodeEditor.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30114.105 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Qkmaxware.Blazor.NodeEditor", "Qkmaxware.Blazor.NodeEditor\Qkmaxware.Blazor.NodeEditor.csproj", "{1AE8341E-B33A-432B-8783-63F85E33FB16}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Test", "Test\Test.csproj", "{4708B66A-95B5-43DB-8E54-961DAE52A0A3}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Debug|x64 = Debug|x64 14 | Debug|x86 = Debug|x86 15 | Release|Any CPU = Release|Any CPU 16 | Release|x64 = Release|x64 17 | Release|x86 = Release|x86 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {1AE8341E-B33A-432B-8783-63F85E33FB16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {1AE8341E-B33A-432B-8783-63F85E33FB16}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {1AE8341E-B33A-432B-8783-63F85E33FB16}.Debug|x64.ActiveCfg = Debug|Any CPU 26 | {1AE8341E-B33A-432B-8783-63F85E33FB16}.Debug|x64.Build.0 = Debug|Any CPU 27 | {1AE8341E-B33A-432B-8783-63F85E33FB16}.Debug|x86.ActiveCfg = Debug|Any CPU 28 | {1AE8341E-B33A-432B-8783-63F85E33FB16}.Debug|x86.Build.0 = Debug|Any CPU 29 | {1AE8341E-B33A-432B-8783-63F85E33FB16}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {1AE8341E-B33A-432B-8783-63F85E33FB16}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {1AE8341E-B33A-432B-8783-63F85E33FB16}.Release|x64.ActiveCfg = Release|Any CPU 32 | {1AE8341E-B33A-432B-8783-63F85E33FB16}.Release|x64.Build.0 = Release|Any CPU 33 | {1AE8341E-B33A-432B-8783-63F85E33FB16}.Release|x86.ActiveCfg = Release|Any CPU 34 | {1AE8341E-B33A-432B-8783-63F85E33FB16}.Release|x86.Build.0 = Release|Any CPU 35 | {4708B66A-95B5-43DB-8E54-961DAE52A0A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {4708B66A-95B5-43DB-8E54-961DAE52A0A3}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {4708B66A-95B5-43DB-8E54-961DAE52A0A3}.Debug|x64.ActiveCfg = Debug|Any CPU 38 | {4708B66A-95B5-43DB-8E54-961DAE52A0A3}.Debug|x64.Build.0 = Debug|Any CPU 39 | {4708B66A-95B5-43DB-8E54-961DAE52A0A3}.Debug|x86.ActiveCfg = Debug|Any CPU 40 | {4708B66A-95B5-43DB-8E54-961DAE52A0A3}.Debug|x86.Build.0 = Debug|Any CPU 41 | {4708B66A-95B5-43DB-8E54-961DAE52A0A3}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {4708B66A-95B5-43DB-8E54-961DAE52A0A3}.Release|Any CPU.Build.0 = Release|Any CPU 43 | {4708B66A-95B5-43DB-8E54-961DAE52A0A3}.Release|x64.ActiveCfg = Release|Any CPU 44 | {4708B66A-95B5-43DB-8E54-961DAE52A0A3}.Release|x64.Build.0 = Release|Any CPU 45 | {4708B66A-95B5-43DB-8E54-961DAE52A0A3}.Release|x86.ActiveCfg = Release|Any CPU 46 | {4708B66A-95B5-43DB-8E54-961DAE52A0A3}.Release|x86.Build.0 = Release|Any CPU 47 | EndGlobalSection 48 | EndGlobal 49 | -------------------------------------------------------------------------------- /Qkmaxware.Blazor.NodeEditor/Components/BasePropertyDrawer.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Microsoft.AspNetCore.Components; 3 | 4 | namespace Qkmaxware.Blazor.NodeEditor.Components { 5 | 6 | public abstract class BasePropertyDrawer : ComponentBase { 7 | [Parameter] public object Instance {get; set;} 8 | [Parameter] public PropertyInfo Property {get; set;} 9 | } 10 | 11 | } -------------------------------------------------------------------------------- /Qkmaxware.Blazor.NodeEditor/Components/FieldDrawer.razor: -------------------------------------------------------------------------------- 1 | @using System.Reflection 2 | 3 | 4 |
5 | @if (Attribute.IsDefined(Property, typeof(CustomPropertyDrawer))) { 6 | var attribute = (CustomPropertyDrawer)Property.GetCustomAttributes(typeof(CustomPropertyDrawer)).First(); 7 | var drawer = attribute.ComponentType; 8 | @if (typeof(BasePropertyDrawer).IsAssignableFrom(drawer)) { 9 | @renderWidget(drawer) 10 | } 11 | } else if (Property.PropertyType == typeof(int)) { 12 | 13 | } else if (Property.PropertyType == typeof(long)) { 14 | 15 | } else if (Property.PropertyType == typeof(float)) { 16 | 17 | } else if (Property.PropertyType == typeof(double)) { 18 | 19 | } else if (Property.PropertyType == typeof(bool)) { 20 | 21 | } else if (Property.PropertyType == typeof(string)) { 22 | 23 | } else if (Property.PropertyType.IsEnum) { 24 | 29 | } 30 |
31 | 32 | @code { 33 | [Parameter] public object Owner {get; set;} 34 | [Parameter] public PropertyInfo Property {get; set;} 35 | 36 | private bool boolean { 37 | get => (bool)Property.GetValue(Owner); 38 | set => Property.SetValue(Owner, value); 39 | } 40 | private string text { 41 | get => (string)Property.GetValue(Owner); 42 | set => Property.SetValue(Owner, value); 43 | } 44 | private int integer { 45 | get => (int)Property.GetValue(Owner); 46 | set => Property.SetValue(Owner, value); 47 | } 48 | private long bigInteger { 49 | get => (long)Property.GetValue(Owner); 50 | set => Property.SetValue(Owner, value); 51 | } 52 | private float real { 53 | get => (float)Property.GetValue(Owner); 54 | set => Property.SetValue(Owner, value); 55 | } 56 | private double bigReal { 57 | get => (double)Property.GetValue(Owner); 58 | set => Property.SetValue(Owner, value); 59 | } 60 | private int enumeration { 61 | get => (int)Property.GetValue(Owner); 62 | set => Property.SetValue(Owner, value); 63 | } 64 | 65 | private RenderFragment renderWidget(Type t) => builder => { 66 | builder.OpenComponent(0, t); 67 | builder.AddAttribute(1, nameof(BasePropertyDrawer.Instance), this.Owner); 68 | builder.AddAttribute(2, nameof(BasePropertyDrawer.Property), this.Property); 69 | builder.CloseComponent(); 70 | }; 71 | } -------------------------------------------------------------------------------- /Qkmaxware.Blazor.NodeEditor/Components/FieldDrawer.razor.css: -------------------------------------------------------------------------------- 1 | .qk-graph-node-field { 2 | width: 100%; 3 | background-color: rgb(49, 67, 96); 4 | color: white; 5 | margin-bottom: 4px; 6 | } -------------------------------------------------------------------------------- /Qkmaxware.Blazor.NodeEditor/Components/ProcessGraphEditor.razor: -------------------------------------------------------------------------------- 1 | @using Qkmaxware.Blazor.NodeEditor 2 | 3 |
4 |
14 |
15 | @if (Graph != null) { 16 | foreach (var node in Graph.GetNodes()) { 17 |
onRightClickNode(e, node)) @oncontextmenu:preventDefault="true" @oncontextmenu:stopPropagation="true" 19 | class="qk-graph-node" 20 | style="width: @(NodeWidthPx)px; top: @(node.Position.Y)px; left: @(node.Position.X)px;"> 21 |
onNodeDragStart(e, node)) @onmousedown:stopPropagation="true"> 22 | node.Collapsed = !node.Collapsed) @onclick:stopPropagation="true"> 23 | @(node.Collapsed ? "\u25B8" : "\u25BE") 24 | 25 | @node.Name 26 |
27 |
28 | @if (!node.Collapsed) { 29 |
30 |
31 | @if (node.Inputs != null) { 32 | foreach (var input in node.Inputs.Enumerate()) { 33 |
34 | @input.Name 35 |
deleteIncomingConnectionsOnPort(node, input)) @onclick:stopPropagation="true">
36 |
37 | } 38 | } 39 |
40 |
41 | @if (node.Outputs != null) { 42 | foreach (var output in node.Outputs.Enumerate()) { 43 |
44 | @output.Name 45 |
startBuildingConnectionOnPort(node, output)) @onclick:stopPropagation="true">
46 |
47 | } 48 | } 49 |
50 |
51 | @if (node is ParametereizedProcessStep pnode) { 52 | var parametres = pnode.GetParametres(); 53 | var properties = parametres.GetType().GetProperties(); 54 | foreach (var property in properties) { 55 |
56 | 57 |
58 | } 59 | } 60 | } 61 |
62 |
63 | } 64 | 65 | @foreach (var startNode in Graph.GetNodes()) { 66 | foreach (var endNode in Graph.GetNodesFromOutgoingEdges(startNode)) { 67 | foreach (var edgeData in Graph.GetAllEdgeData(startNode, endNode)) { 68 | if (startNode.Outputs != null && endNode.Inputs != null) { 69 | var start = portLocation(startNode, edgeData.FromPortName, isOutputPort: true); 70 | var end = portLocation(endNode, edgeData.ToPortName, isOutputPort: false); 71 | 72 | } 73 | } 74 | } 75 | } 76 | @if (startConnectionAtNode != null) { 77 | var start = portLocation(startConnectionAtNode, startConnectionAtPort.Name, isOutputPort: true); 78 | 79 | } 80 | 81 | } 82 |
83 |
84 |
85 | @if (contextForNode == null) { 86 |
Add
87 | @if (NodeTypes != null) { 88 | foreach(var generator in NodeTypes) { 89 |
newNodeAtPosition(e, generator))>@generator.GeneratorName()
90 | } 91 | } 92 | } else { 93 |
Edit
94 |
Delete
95 | } 96 |
97 |
98 | @if (error != null) { 99 |
100 | 101 | @error.Message 102 | 103 | 104 |
105 | } 106 | @if (ChildContent != null) { 107 | @ChildContent 108 | } 109 |
110 |
111 | 112 | @code { 113 | [Parameter] public bool AllowEdit {get; set;} 114 | [Parameter] public string WidthCss {get; set;} = "100vw"; 115 | [Parameter] public string HeightCss {get; set;} = "100vh"; 116 | [Parameter] public ProcessGraph Graph {get; set;} 117 | [Parameter] public INodeGenerator[] NodeTypes {get; set;} 118 | [Parameter] public RenderFragment ChildContent {get; set;} 119 | 120 | private int width => Graph == null ? 0 : (Graph.GetNodes().Select(x => x.Position.X).Max() + NodeWidthPx); 121 | private int height => Graph == null ? 0 : (Graph.GetNodes().Select(x => x.Position.Y).Max() + NodeWidthPx); 122 | 123 | public static readonly int HeaderHeightPx = 32; 124 | public static readonly int NodeWidthPx = 240; 125 | public static readonly int NodePortHeightPx = 24; 126 | 127 | private Exception error; 128 | private int scrollX; 129 | private int scrollY; 130 | private int zoom = 1; 131 | 132 | private (int X, int Y) getMouseLocation(MouseEventArgs e) { 133 | var invScale = 1.0f/this.zoom; 134 | var newMouseX = (int)((e.ClientX - scrollX) * invScale); 135 | var newMouseY = (int)((e.ClientY - scrollY) * invScale); 136 | return (X: newMouseX, Y: newMouseY); 137 | } 138 | 139 | private bool dragging = false; 140 | private double[] clientPosition; 141 | private int[] cachedPosition; 142 | private ProcessStep heldNode; 143 | private void onMouseDragStart(MouseEventArgs e) { 144 | if (!dragging) { 145 | clientPosition = new double[]{ e.ClientX, e.ClientY }; 146 | cachedPosition = new int[]{ this.scrollX, this.scrollY }; 147 | dragging = true; 148 | } 149 | } 150 | 151 | private int mouseX; 152 | private int mouseY; 153 | private void onMouseDrag(MouseEventArgs e) { 154 | var pos = getMouseLocation(e); 155 | mouseX = pos.X; 156 | mouseY = pos.Y; 157 | 158 | if (!dragging) 159 | return; 160 | 161 | // Below this is to move the image 162 | if (this.cachedPosition == null || this.clientPosition == null) { 163 | // If mouse is not pressed 164 | return; 165 | } 166 | 167 | var x = cachedPosition[0] + (int)(e.ClientX - clientPosition[0]); 168 | var y = cachedPosition[1] + (int)(e.ClientY - clientPosition[1]); 169 | 170 | if (heldNode == null) { 171 | this.scrollX = x; 172 | this.scrollY = y; 173 | } else { 174 | this.heldNode.Position.X = Math.Max(0, x); // Prevent below 0 175 | this.heldNode.Position.Y = Math.Max(0, y); // Prevent below 0 176 | } 177 | } 178 | 179 | private void onMouseDragEnd() { 180 | this.clientPosition = null; 181 | this.cachedPosition = null; 182 | this.heldNode = null; 183 | dragging = false; 184 | } 185 | 186 | private void onNodeDragStart(MouseEventArgs e, ProcessStep node) { 187 | if (!AllowEdit) 188 | return; 189 | 190 | clientPosition = new double[]{ e.ClientX, e.ClientY }; 191 | cachedPosition = new int[]{ node.Position.X, node.Position.Y }; 192 | this.heldNode = node; 193 | dragging = true; 194 | } 195 | 196 | private int contextMenuX; 197 | private int contextMenuY; 198 | private bool contextMenuHidden = true; 199 | private void onClickBackground(MouseEventArgs e) { 200 | contextMenuHidden = true; 201 | contextForNode = null; 202 | this.startConnectionAtNode = null; 203 | this.startConnectionAtPort = null; 204 | } 205 | private void onRightClickBackground(MouseEventArgs e) { 206 | if (AllowEdit && e.Button == 2) { 207 | contextMenuHidden = false; 208 | contextMenuX = (int)e.ClientX; 209 | contextMenuY = (int)e.ClientY; 210 | } else { 211 | contextMenuHidden = true; 212 | } 213 | } 214 | private ProcessStep contextForNode; 215 | private void onRightClickNode(MouseEventArgs e, ProcessStep node) { 216 | if (AllowEdit && e.Button == 2) { 217 | contextMenuHidden = false; 218 | contextMenuX = (int)e.ClientX; 219 | contextMenuY = (int)e.ClientY; 220 | contextForNode = node; 221 | 222 | } else { 223 | contextMenuHidden = true; 224 | } 225 | } 226 | 227 | private void newNodeAtPosition(MouseEventArgs e, INodeGenerator generator) { 228 | if (!AllowEdit) 229 | return; 230 | 231 | var node = generator.Generate(); 232 | var position = getMouseLocation(e); 233 | node.Position = new Point { 234 | X = position.X, 235 | Y = position.Y 236 | }; 237 | this.Graph.AddNode(node); 238 | } 239 | private void deleteContextNode() { 240 | if (AllowEdit && contextForNode != null) { 241 | this.Graph.RemoveNode(contextForNode); 242 | } 243 | } 244 | 245 | private ProcessStep startConnectionAtNode; 246 | private NodePort startConnectionAtPort; 247 | private void deleteIncomingConnectionsOnPort(ProcessStep node, NodePort port) { 248 | if (!AllowEdit) 249 | return; 250 | 251 | if (this.startConnectionAtNode != null) { 252 | try { 253 | Graph.Connect(startConnectionAtNode, node, new NodePortReference{ 254 | FromPortName = startConnectionAtPort.Name, 255 | ToPortName = port.Name 256 | }); 257 | this.startConnectionAtNode = null; 258 | this.startConnectionAtPort = null; 259 | } catch (Exception e) { 260 | this.error = e; 261 | } 262 | } else { 263 | Graph.DisconnectAll((linkStart, linkEnd, portData) => linkEnd == node && portData.ToPortName == port.Name); 264 | } 265 | } 266 | private void startBuildingConnectionOnPort(ProcessStep node, NodePort port) { 267 | if (!AllowEdit) 268 | return; 269 | 270 | if (this.startConnectionAtNode == null) { 271 | this.startConnectionAtNode = node; 272 | this.startConnectionAtPort = port; 273 | } else { 274 | this.startConnectionAtNode = null; 275 | this.startConnectionAtPort = null; 276 | } 277 | } 278 | 279 | private Point portLocation(ProcessStep node, string port, bool isOutputPort) { 280 | var x = node.Position.X + (isOutputPort ? NodeWidthPx : 0); 281 | 282 | var header = HeaderHeightPx; 283 | var buffer = 4 + NodePortHeightPx/2; 284 | var containerStartOffersetY = header + buffer; 285 | var nodeIndex = isOutputPort ? node.Outputs.IndexOf(port) : node.Inputs.IndexOf(port); 286 | 287 | if (node.Collapsed || nodeIndex < 0) { 288 | var y = node.Position.Y + containerStartOffersetY; 289 | return new Point { 290 | X = x, 291 | Y = y, 292 | }; 293 | } 294 | else { 295 | var y = node.Position.Y + containerStartOffersetY + NodePortHeightPx * nodeIndex; 296 | return new Point { 297 | X = x, 298 | Y = y, 299 | }; 300 | } 301 | } 302 | } -------------------------------------------------------------------------------- /Qkmaxware.Blazor.NodeEditor/Components/ProcessGraphEditor.razor.css: -------------------------------------------------------------------------------- 1 | .qk-graph-container { 2 | position: relative; 3 | overflow: hidden; 4 | background-color: rgb(36, 35, 44); 5 | color: white; 6 | } 7 | 8 | .qk-graph-node { 9 | position: absolute; 10 | border-radius: 15px; 11 | background-color: rgb(59, 59, 59); 12 | } 13 | 14 | .qk-graph-node-header { 15 | font-weight: bold; 16 | border-radius: 15px 15px 0 0; 17 | background-color: rgb(234, 37, 81); 18 | padding: 4px 8px; 19 | overflow: hidden; 20 | white-space: nowrap; 21 | user-select: none; 22 | } 23 | 24 | .qk-graph-node-content { 25 | padding: 4px 8px; 26 | min-height: 48px; 27 | } 28 | 29 | .qk-graph-node-input { 30 | display: relative; 31 | overflow: hidden; 32 | padding: 2px 6px; 33 | } 34 | 35 | .qk-graph-node-input-circle { 36 | position: absolute; 37 | 38 | content: "+"; 39 | height: 24px; 40 | width: 24px; 41 | background-color: rgb(236, 183, 69); 42 | border-color: rgb(190, 142, 38); 43 | border-radius: 1px; 44 | border-style: solid; 45 | border-radius: 50%; 46 | display: inline-block; 47 | } 48 | 49 | .qk-left .qk-graph-node-input-circle { 50 | left: -12px; 51 | } 52 | 53 | .qk-right .qk-graph-node-input-circle { 54 | right: -12px; 55 | } 56 | 57 | .qk-graph-node-row { 58 | display: table; 59 | width: 100%; 60 | } 61 | 62 | .qk-graph-node-half { 63 | width: 49.9%; 64 | vertical-align: top; 65 | display: inline-block; 66 | } 67 | 68 | .qk-right { 69 | text-align: right; 70 | } 71 | 72 | .qk-graph-context-menu { 73 | position: absolute; 74 | padding: 8px; 75 | background-color: rgb(49, 67, 96); 76 | } 77 | 78 | .qk-graph-context-item { 79 | padding: 2px 6px; 80 | cursor: pointer; 81 | } 82 | 83 | .qk-graph-context-item:hover { 84 | background-color: rgb(73, 86, 107); 85 | } 86 | 87 | .qk-graph-overlay { 88 | position: absolute; 89 | pointer-events: none; 90 | left: 0; right: 0; top: 0; bottom: 0; 91 | } 92 | 93 | .qk-graph-notification { 94 | position: fixed; 95 | pointer-events: all; 96 | right: 25px; 97 | bottom: 86px; 98 | padding: 12px; 99 | z-index: 1; 100 | background-color: rgb(44, 44, 44); 101 | } 102 | 103 | .qk-graph-notification button { 104 | border: none; 105 | border-left: 2px solid white; 106 | background-color: transparent; 107 | color: white; 108 | padding: 4px 8px; 109 | text-align: center; 110 | text-decoration: none; 111 | display: inline-block; 112 | font-size: 16px; 113 | } 114 | .qk-graph-notification button:hover { 115 | background-color: rgb(234, 37, 81); 116 | } -------------------------------------------------------------------------------- /Qkmaxware.Blazor.NodeEditor/Qkmaxware.Blazor.NodeEditor.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Qkmaxware.Blazor.NodeEditor 5 | 1.0.0 6 | Colin Halseth 7 | blazor, node-editor 8 | LICENSE.md 9 | https://github.com/qkmaxware/Blazor-NodeEditor.git 10 | git 11 | 12 | A reusable component for viewing and editing node editor style graphs allowing for custom node types. 13 | 14 | ./../nupkg 15 | 16 | 17 | net5.0 18 | Qkmaxware.Blazor.NodeEditor 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /Qkmaxware.Blazor.NodeEditor/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Components.Web 2 | -------------------------------------------------------------------------------- /Qkmaxware.Blazor.NodeEditor/data/CustomPropertyDrawer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Qkmaxware.Blazor.NodeEditor.Components; 3 | 4 | namespace Qkmaxware.Blazor.NodeEditor { 5 | 6 | [AttributeUsage(AttributeTargets.Property)] 7 | public class CustomPropertyDrawer : Attribute { 8 | public Type ComponentType {get; private set;} 9 | public CustomPropertyDrawer(Type drawer) { 10 | if (! (typeof(BasePropertyDrawer).IsAssignableFrom(drawer)) ) { 11 | throw new ArgumentException($"Custom property drawers must extend from {typeof(BasePropertyDrawer)}"); 12 | } 13 | this.ComponentType = drawer; 14 | } 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /Qkmaxware.Blazor.NodeEditor/data/Graph.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Qkmaxware.Blazor.NodeEditor { 6 | 7 | public class Graph where Edge:class { 8 | private class EdgeLink { 9 | public int EndpointIndex; 10 | public Edge Data; 11 | } 12 | private List nodes = new List(); 13 | private List> edges = new List>(); 14 | 15 | public void Clear() { 16 | this.nodes = new List(); 17 | this.edges = new List>(); 18 | } 19 | public void AddNode(Node node) { 20 | // Add the node 21 | this.nodes.Add(node); 22 | // Add an empty list of edges 23 | this.edges.Add(new List()); 24 | } 25 | 26 | public void RemoveNode(Node node){ 27 | var index = this.nodes.IndexOf(node); 28 | if (index >= 0) { 29 | // Delete this node and its outgoing edges 30 | this.nodes.RemoveAt(index); 31 | this.edges.RemoveAt(index); 32 | 33 | // Delete all inbound edges from other nodes (updating the indexes of subsequent connections) 34 | for (var startNodeIndex = 0; startNodeIndex < edges.Count; startNodeIndex++) { 35 | var outboundEdges = edges[startNodeIndex]; 36 | outboundEdges.RemoveAll(edge => edge.EndpointIndex == index); 37 | foreach (var edge in outboundEdges) { 38 | if (edge.EndpointIndex > index) { 39 | edge.EndpointIndex--; 40 | } 41 | } 42 | } 43 | } 44 | } 45 | public IEnumerable GetNodesOfType() { 46 | return this.nodes.OfType(); 47 | } 48 | public IEnumerable GetNodes() { 49 | return nodes.AsReadOnly(); 50 | } 51 | public IEnumerable GetNodesFromOutgoingEdges(Node node) { 52 | var index = nodes.IndexOf(node); 53 | if (index >= 0) { 54 | return edges[index].Select(edge => nodes[edge.EndpointIndex]); 55 | } else { 56 | return Enumerable.Empty(); 57 | } 58 | } 59 | public IEnumerable GetNodesFromIncomingEdges(Node node) { 60 | var index = nodes.IndexOf(node); 61 | if (index >= 0) { 62 | return edges.SelectMany( 63 | (outbound, outboundIndex) => 64 | outbound 65 | .Where(edge => edge.EndpointIndex == index) 66 | .Select(edge => nodes[outboundIndex]) 67 | ); 68 | } else { 69 | return Enumerable.Empty(); 70 | } 71 | } 72 | public virtual bool Connect (Node start, Node end, Edge data = null) { 73 | var startIndex = nodes.IndexOf(start); 74 | var endIndex = nodes.IndexOf(end); 75 | if (startIndex >= 0 && endIndex >= 0) { 76 | var edge = new EdgeLink { 77 | EndpointIndex = endIndex, 78 | Data = data, 79 | }; 80 | this.edges[startIndex].Add(edge); 81 | return true; 82 | } else { 83 | return false; 84 | } 85 | } 86 | public bool Disconnect(Node start, Node end) { 87 | var startIndex = nodes.IndexOf(start); 88 | var endIndex = nodes.IndexOf(end); 89 | if (startIndex >= 0 && endIndex >= 0) { 90 | var removed = edges[startIndex].RemoveAll(edge => edge.EndpointIndex == endIndex); 91 | return removed > 0; 92 | } else { 93 | return false; 94 | } 95 | } 96 | public void DisconnectAll(Func matcher) { 97 | for (var i = 0; i < edges.Count; i++) { 98 | edges[i].RemoveAll((link) => matcher(nodes[i], nodes[link.EndpointIndex], link.Data)); 99 | } 100 | } 101 | public Edge GetEdgeData(Node start, Node end) { 102 | return GetAllEdgeData(start, end).FirstOrDefault(); 103 | } 104 | public IEnumerable GetAllEdgeData(Node start, Node end) { 105 | var startIndex = nodes.IndexOf(start); 106 | var endIndex = nodes.IndexOf(end); 107 | if (startIndex >= 0 && endIndex >= 0) { 108 | return this.edges[startIndex].Where(edge => edge.EndpointIndex == endIndex).Select(edge => edge.Data); 109 | } else { 110 | return Enumerable.Empty(); 111 | } 112 | } 113 | } 114 | 115 | public class Graph : Graph {} 116 | 117 | } -------------------------------------------------------------------------------- /Qkmaxware.Blazor.NodeEditor/data/NodeGenerator.cs: -------------------------------------------------------------------------------- 1 | namespace Qkmaxware.Blazor.NodeEditor { 2 | 3 | public interface INodeGenerator { 4 | string GeneratorName(); 5 | NodeType Generate(); 6 | } 7 | 8 | } -------------------------------------------------------------------------------- /Qkmaxware.Blazor.NodeEditor/data/Nodes.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | namespace Qkmaxware.Blazor.NodeEditor { 5 | 6 | public struct Point { 7 | public int X; 8 | public int Y; 9 | } 10 | 11 | public class BaseNode { 12 | public string Name; 13 | public string Description; 14 | public bool Collapsed = false; 15 | public Point Position; 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /Qkmaxware.Blazor.NodeEditor/data/ProcessGraph.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Qkmaxware.Blazor.NodeEditor { 6 | 7 | public class NodePortReference { 8 | public string FromPortName; 9 | public string ToPortName; 10 | } 11 | 12 | public class ProcessGraph : Graph { 13 | public IEnumerable GetOutputs() { 14 | return this.GetNodes().Where(node => node.Outputs == null || node.Outputs.Size == 0); 15 | } 16 | public IEnumerable GetOutputsOfType() { 17 | return this.GetOutputs().OfType(); 18 | } 19 | 20 | public bool Connect(ProcessStep start, NodePort outputPort, ProcessStep end, NodePort inputPort) { 21 | return Connect(start, end, new NodePortReference { 22 | FromPortName = outputPort.Name, 23 | ToPortName = inputPort.Name, 24 | }); 25 | } 26 | public override bool Connect (ProcessStep start, ProcessStep end, NodePortReference data) { 27 | // Validation 28 | if (data == null) { 29 | // Verify port mapping exists 30 | throw new ArgumentException("Missing port connection references"); 31 | } 32 | if (string.IsNullOrEmpty(data.FromPortName)) { 33 | // Verify port mapping is valid 34 | throw new ArgumentException("Missing start node's output port name"); 35 | } 36 | if (string.IsNullOrEmpty(data.ToPortName)) { 37 | // Verify port mapping is valid 38 | throw new ArgumentException("Missing end node's input port name"); 39 | } 40 | var outputPort = start.Outputs[data.FromPortName]; 41 | if (outputPort == null) { 42 | // Verify port exists 43 | throw new ArgumentException($"Start node doesn't have an output port named '{data.FromPortName}'"); 44 | } 45 | var inputPort = end.Inputs[data.ToPortName]; 46 | if (inputPort == null) { 47 | // Verify port exists 48 | throw new ArgumentException($"End node doesn't have an input port named '{data.ToPortName}'"); 49 | } 50 | if (!inputPort.CanStore(outputPort.GetStorageType())) { 51 | // Verify output port is compatible with input port 52 | throw new ArgumentException($"Cannot connect port output of type {outputPort.GetStorageType()} to one accepting {inputPort.GetStorageType()}"); 53 | } 54 | 55 | // Clean up extra connections to the same port 56 | this.DisconnectAll((linkStart, linkEnd, mapping) => linkEnd == end && mapping.ToPortName == data.ToPortName); 57 | 58 | // Return 59 | return base.Connect(start, end, data); 60 | } 61 | 62 | private void recompute(ProcessStep node) { 63 | if (node == null) 64 | return; 65 | 66 | // Compute this node's inputs (recursively) 67 | var inputs = this.GetNodesFromIncomingEdges(node); 68 | foreach (var input in inputs) { 69 | if (!input.HasCachedOutput()) { 70 | recompute(input); 71 | } 72 | } 73 | 74 | // Set the inputs of the current node 75 | foreach (var input in inputs) { 76 | foreach (var edge in this.GetAllEdgeData(input, node)) { 77 | var inputPort = node.Inputs[edge.ToPortName]; 78 | var outputPort = input.Outputs[edge.FromPortName]; 79 | if (inputPort != null && outputPort != null) { 80 | inputPort.Store(outputPort.Fetch()); 81 | } 82 | } 83 | } 84 | 85 | // Compute the node's value 86 | node.Recalculate(); 87 | } 88 | 89 | public void Recalculate(ProcessStep step) { 90 | // Clear old results 91 | foreach (var node in this.GetNodes()) { 92 | node.Reset(); 93 | } 94 | 95 | recompute(step); 96 | } 97 | public void RecalculateAll() { 98 | // Clear old results 99 | foreach (var node in this.GetNodes()) { 100 | node.Reset(); 101 | } 102 | 103 | // Travel graph computing new results 104 | foreach (var node in GetOutputs()) { 105 | recompute(node); 106 | } 107 | } 108 | } 109 | 110 | } -------------------------------------------------------------------------------- /Qkmaxware.Blazor.NodeEditor/data/ProcessStep.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Collections.Generic; 4 | 5 | namespace Qkmaxware.Blazor.NodeEditor { 6 | 7 | public class NodePort { 8 | public string Name; 9 | public bool HasValue {get; private set;} 10 | private object value; 11 | protected virtual bool CanStore(object value) { 12 | return true; 13 | } 14 | public virtual Type GetStorageType() => typeof(object); 15 | public virtual bool CanStore (Type t) { 16 | return true; 17 | } 18 | public void Store(object value) { 19 | if (CanStore(value)) { 20 | this.value = value; 21 | this.HasValue = true; 22 | } 23 | } 24 | public object Fetch() { 25 | return value; 26 | } 27 | public R Fetch() { 28 | if (!HasValue) 29 | return default(R); 30 | 31 | if (value is R rvalue) { 32 | return rvalue; 33 | } else { 34 | return default(R); 35 | } 36 | } 37 | public void Clear() { 38 | this.HasValue = false; 39 | this.value = null; 40 | } 41 | } 42 | 43 | public class NodePort : NodePort { 44 | protected override bool CanStore(object o) { 45 | return o is T; 46 | } 47 | public override Type GetStorageType() => typeof(T); 48 | public override bool CanStore (Type t) { 49 | return typeof(T).IsAssignableFrom(t); 50 | } 51 | } 52 | 53 | public class NodePortCollection { 54 | public int Size => ports.Count; 55 | private List ports = new List(); 56 | public NodePort this[string name] => ports.Where(port => port.Name == name).FirstOrDefault(); 57 | public NodePort this[int index] => ports[index]; 58 | 59 | public NodePortCollection() {} 60 | public NodePortCollection(IEnumerable ports) { 61 | this.ports = ports.ToList(); 62 | } 63 | public int IndexOf(string port) { 64 | for (var i = 0; i < ports.Count; i++) { 65 | if (ports[i].Name == port) 66 | return i; 67 | } 68 | return -1; 69 | } 70 | public IEnumerable Enumerate() { 71 | foreach (var port in ports) 72 | yield return port; 73 | } 74 | } 75 | 76 | public abstract class ProcessStep : BaseNode { 77 | public NodePortCollection Inputs; 78 | public NodePortCollection Outputs; 79 | 80 | /// 81 | /// Check if this node has already computed and cached its outputs 82 | /// 83 | /// True if the outputs are already computed, false otherwise 84 | public virtual bool HasCachedOutput() { 85 | if (Outputs != null) { 86 | foreach (var output in Outputs.Enumerate()) { 87 | if (!output.HasValue) 88 | return false; 89 | } 90 | } 91 | return true; 92 | } 93 | /// 94 | /// Reset the inputs and outputs clearing all computed values 95 | /// 96 | public void Reset() { 97 | if (Inputs != null) { 98 | foreach (var input in Inputs.Enumerate()) { 99 | input.Clear(); 100 | } 101 | } 102 | if (Outputs != null) { 103 | foreach (var output in Outputs.Enumerate()) { 104 | output.Clear(); 105 | } 106 | } 107 | } 108 | /// 109 | /// Force the node to recalculate all of its outputs 110 | /// 111 | public abstract void Recalculate(); 112 | } 113 | 114 | public abstract class ParametereizedProcessStep : ProcessStep { 115 | public abstract object GetParametres(); 116 | } 117 | public abstract class ParametreizedProcessStep : ParametereizedProcessStep where ParamType:class { 118 | public ParamType Parametres {get; set;} 119 | public override object GetParametres() => Parametres; 120 | } 121 | 122 | } -------------------------------------------------------------------------------- /Qkmaxware.Blazor.NodeEditor/wwwroot/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qkmaxware/Blazor-NodeEditor/85050293507e551483006cdd05b6323a4906217d/Qkmaxware.Blazor.NodeEditor/wwwroot/.gitkeep -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # NodeEditor 2 | Qkmaxware.Blazor.NodeEditor is a .net 5 library for Blazor which provides a reusable component for creating node-editor user interfaces. 3 | 4 | Node-editors are graphical editors designed allows users to compose complicated sequences of events while hiding the computational complexity of each of the individual steps. It does this by creating "nodes" representing each step in a process. Edges connect the outputs of one node to the inputs of another node allowing data to move through the process until it reaches the end. 5 | 6 | - [NodeEditor](#nodeeditor) 7 | - [License](#license) 8 | - [Example](#example) 9 | - [Tutorials](#tutorials) 10 | - [Creating Custom Node Types](#creating-custom-node-types) 11 | - [Nodes With User Defined Parameters](#nodes-with-user-defined-parameters) 12 | - [Custom Property Drawers](#custom-property-drawers) 13 | 14 | 15 | ## License 16 | See [License](LICENSE.md) for license details. 17 | 18 | ## Example 19 | An example of how to use and customize the node-editor can be shown in the [Test](Test) directory of this repo. 20 | 21 | ![](Test/preview.png) 22 | 23 | ## Tutorials 24 | ### Creating Custom Node Types 25 | No types of nodes are provided in this library as the types of nodes that are used are specific to each use case. You can see a larger example in the [Test](Test\Data\NodeTypes.cs) project. All nodes for processes must extend from the **ProcessStep** class. The constructor for each node type should declare the allowed inputs and outputs for that node. In the **Recalculate** method all the outputs should have their values set or else the system may try to re-compute the outputs each time it is used rather than using cached results. 26 | ```cs 27 | public class AdditionNode : ProcessStep { 28 | public AdditionNode() { 29 | this.Name = "Addition"; 30 | this.Inputs = new NodePortCollection(new NodePort[]{ 31 | new NodePort { 32 | Name = "first" 33 | }, 34 | new NodePort { 35 | Name = "second" 36 | } 37 | }); 38 | this.Outputs = new NodePortCollection(new NodePort[]{ 39 | new NodePort { 40 | Name = "value" 41 | } 42 | }); 43 | } 44 | 45 | public override void Recalculate() { 46 | var t1 = this.Inputs["first"].Fetch(); 47 | var t2 = this.Inputs["second"].Fetch(); 48 | 49 | this.Outputs["value"].Store(t1 + t2) 50 | } 51 | } 52 | ``` 53 | ### Nodes With User Defined Parameters 54 | In may contexts, it becomes important to ask the process designer for information that is important to how the process run. For example maybe choosing an image file for editing, changing the freqency of a noise generator, or choosing an email address to send reports to. These are provided by the **ParametreizedProcessStep** class. The generic argument determines a data-class which is used as the parameter set for the node. This allows the node access to a new variable **Parametres** which will automatically bind all public properties to input fields in Blazor. 55 | ```cs 56 | public class MyNodeParams { 57 | public float MyFloat {get; set;} 58 | public bool MyBool {get; set;} 59 | public string MyText {get; set;} 60 | } 61 | public class MyNode : ParametreizedProcessStep { 62 | public MyNode() { 63 | this.Parametres = new MyNodeParams(); 64 | ... 65 | } 66 | } 67 | ``` 68 | By default the following property types for a node's parametres can be automatically bound to Blazor input fields. 69 | | Type | HTML Field Type | 70 | |------|------------| 71 | | int | number | 72 | | long | number | 73 | | float | number | 74 | | double | number | 75 | | bool | checkbox | 76 | | string | text | 77 | | enum | select | 78 | 79 | #### Custom Property Drawers 80 | Sometimes there may be situations where you want to provide a property for an input parameter but its for a type that is not supported by default (like a file loader). In these cases you will need to provide a custom property drawer to tell the node-editor how to draw and bind that property. This is done by annotating the property with **CustomPropertyDrawer** annotation and providing it with the type of a class that extends from **BasePropertyDrawer** which will be drawn in place of an input field. This base class provides two protected fields **Instance** which coorelates to the object instance the parametre value exists within and **Property** which is the property type reflection of the exact property being drawn. 81 | ```cs 82 | public class CustomPropertyParams { 83 | [CustomPropertyDrawer(typeof(BitmapPropertyDrawer))] 84 | public Uri Image {get; set;} 85 | } 86 | public class MyCustomPropertyNode : ParametreizedProcessStep { 87 | ... 88 | } 89 | ``` 90 | ```html 91 | // BitmapPropertyDrawer.razor 92 | @using Qkmaxware.Blazor.NodeEditor.Components 93 | @inherits BasePropertyDrawer 94 | 95 | 96 | ``` -------------------------------------------------------------------------------- /Test/App.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 |

Sorry, there's nothing at this address.

8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /Test/Data/NodeTypes.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Qkmaxware.Blazor.NodeEditor; 3 | using Test.Shared; 4 | 5 | namespace Test.Data { 6 | 7 | public class SimpleNodeGenerator : INodeGenerator { 8 | private string name; 9 | private Func constructor; 10 | public SimpleNodeGenerator(string name, Func constructor) { 11 | this.name = name; 12 | this.constructor = constructor; 13 | } 14 | public string GeneratorName() => name; 15 | public ProcessStep Generate() => constructor(); 16 | } 17 | 18 | public class InputNode : ParametreizedProcessStep { 19 | public class ParameterSet { 20 | public float Value {get; set;} 21 | } 22 | public InputNode() { 23 | this.Name = "Input"; 24 | this.Parametres = new ParameterSet(); 25 | 26 | this.Inputs = null; 27 | this.Outputs = new NodePortCollection(new NodePort[]{ 28 | new NodePort { 29 | Name = "value" 30 | } 31 | }); 32 | } 33 | public override void Recalculate() { 34 | this.Outputs["value"].Store(this.Parametres.Value); 35 | Console.WriteLine("Pushing " + this.Parametres.Value); 36 | } 37 | } 38 | 39 | public class OutputNode : ParametreizedProcessStep { 40 | public class ParameterSet { 41 | [CustomPropertyDrawer(typeof(OutputDrawer))] 42 | public float Result {get; set;} 43 | } 44 | 45 | public OutputNode() { 46 | this.Parametres = new ParameterSet(); 47 | this.Name = "Output"; 48 | this.Inputs = new NodePortCollection(new NodePort[]{ 49 | new NodePort { 50 | Name = "value" 51 | } 52 | }); 53 | } 54 | public override void Recalculate() { 55 | this.Parametres.Result = this.Inputs["value"].Fetch(); 56 | Console.WriteLine("Computed Result " + this.Parametres.Result); 57 | } 58 | } 59 | 60 | public class AddNode : ProcessStep { 61 | public AddNode() { 62 | this.Name = "Addition"; 63 | this.Inputs = new NodePortCollection(new NodePort[]{ 64 | new NodePort { 65 | Name = "first" 66 | }, 67 | new NodePort { 68 | Name = "second" 69 | } 70 | }); 71 | this.Outputs = new NodePortCollection(new NodePort[]{ 72 | new NodePort { 73 | Name = "value" 74 | } 75 | }); 76 | } 77 | 78 | public override void Recalculate() { 79 | var t1 = this.Inputs["first"].Fetch(); 80 | var t2 = this.Inputs["second"].Fetch(); 81 | 82 | this.Outputs["value"].Store(t1 + t2); 83 | Console.WriteLine("Adding " + t1 + " + " + t2 ); 84 | } 85 | } 86 | 87 | } -------------------------------------------------------------------------------- /Test/Pages/Error.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model Test.Pages.ErrorModel 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Error 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |

Error.

19 |

An error occurred while processing your request.

20 | 21 | @if (Model.ShowRequestId) 22 | { 23 |

24 | Request ID: @Model.RequestId 25 |

26 | } 27 | 28 |

Development Mode

29 |

30 | Swapping to the Development environment displays detailed information about the error that occurred. 31 |

32 |

33 | The Development environment shouldn't be enabled for deployed applications. 34 | It can result in displaying sensitive information from exceptions to end users. 35 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 36 | and restarting the app. 37 |

38 |
39 |
40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /Test/Pages/Error.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.AspNetCore.Mvc.RazorPages; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace Test.Pages 11 | { 12 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 13 | [IgnoreAntiforgeryToken] 14 | public class ErrorModel : PageModel 15 | { 16 | public string RequestId { get; set; } 17 | 18 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 19 | 20 | private readonly ILogger _logger; 21 | 22 | public ErrorModel(ILogger logger) 23 | { 24 | _logger = logger; 25 | } 26 | 27 | public void OnGet() 28 | { 29 | RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Test/Pages/Index.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | @using Qkmaxware.Blazor.NodeEditor.Components 3 | @using Qkmaxware.Blazor.NodeEditor 4 | 5 | 6 |
7 | 8 |
9 |
10 | 11 | @code { 12 | private ProcessGraph graph; 13 | private INodeGenerator[] nodeTypes; 14 | private OutputNode resultNode; 15 | protected override void OnInitialized() { 16 | this.graph = new ProcessGraph(); 17 | this.nodeTypes = new INodeGenerator[]{ 18 | new SimpleNodeGenerator("Input Number", () => new InputNode()), 19 | new SimpleNodeGenerator("Addition", () => new AddNode()) 20 | }; 21 | 22 | var input1 = new InputNode(); 23 | input1.Position = new Point { 24 | X = 0, 25 | Y = 0 26 | }; 27 | var input2 = new InputNode(); 28 | input2.Position = new Point { 29 | X = 0, 30 | Y = 256 31 | }; 32 | var op = new AddNode(); 33 | op.Position = new Point{ 34 | X = 320, 35 | Y = 180, 36 | }; 37 | var output = new OutputNode(); 38 | output.Position = new Point { 39 | X = 600, 40 | Y = 180 41 | }; 42 | resultNode = output; 43 | 44 | this.graph.AddNode(input1); 45 | this.graph.AddNode(input2); 46 | this.graph.AddNode(op); 47 | this.graph.AddNode(output); 48 | 49 | this.graph.Connect(input1, op, new NodePortReference { 50 | FromPortName = "value", 51 | ToPortName = "first" 52 | }); 53 | this.graph.Connect(input2, op, new NodePortReference { 54 | FromPortName = "value", 55 | ToPortName = "second" 56 | }); 57 | this.graph.Connect(op, output, new NodePortReference { 58 | FromPortName = "value", 59 | ToPortName = "value" 60 | }); 61 | } 62 | 63 | private void getResult() { 64 | this.graph.Recalculate(this.resultNode); 65 | StateHasChanged(); 66 | } 67 | } -------------------------------------------------------------------------------- /Test/Pages/_Host.cshtml: -------------------------------------------------------------------------------- 1 | @page "/" 2 | @namespace Test.Pages 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | @{ 5 | Layout = null; 6 | } 7 | 8 | 9 | 10 | 11 | 12 | 13 | Test 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | An error has occurred. This application may no longer respond until reloaded. 25 | 26 | 27 | An unhandled exception has occurred. See browser dev tools for details. 28 | 29 | Reload 30 | 🗙 31 |
32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /Test/Pages/index.razor.css: -------------------------------------------------------------------------------- 1 | .action-bar { 2 | position: absolute; 3 | bottom: 0; left: 0; right: 0; 4 | background-color: black; 5 | padding: 12px; 6 | pointer-events: all; 7 | } 8 | 9 | button { 10 | background-color: red; /* Green */ 11 | border: none; 12 | color: white; 13 | padding: 6px 12px; 14 | text-align: center; 15 | text-decoration: none; 16 | display: inline-block; 17 | font-size: 16px; 18 | } 19 | 20 | button:hover { 21 | background-color: rgb(240, 71, 71); 22 | } -------------------------------------------------------------------------------- /Test/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.Hosting; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace Test 11 | { 12 | public class Program 13 | { 14 | public static void Main(string[] args) 15 | { 16 | CreateHostBuilder(args).Build().Run(); 17 | } 18 | 19 | public static IHostBuilder CreateHostBuilder(string[] args) => 20 | Host.CreateDefaultBuilder(args) 21 | .ConfigureWebHostDefaults(webBuilder => 22 | { 23 | webBuilder.UseStartup(); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Test/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:11559", 7 | "sslPort": 44344 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "Test": { 19 | "commandName": "Project", 20 | "dotnetRunMessages": "true", 21 | "launchBrowser": true, 22 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Test/Shared/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 |
4 | @Body 5 |
6 | -------------------------------------------------------------------------------- /Test/Shared/OutputDrawer.razor: -------------------------------------------------------------------------------- 1 | @using Qkmaxware.Blazor.NodeEditor.Components 2 | @inherits BasePropertyDrawer 3 | 4 | 5 | 6 | @code { 7 | float value { 8 | get => (float)Property.GetValue(Instance); 9 | set {} 10 | } 11 | } -------------------------------------------------------------------------------- /Test/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Components; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.AspNetCore.HttpsPolicy; 9 | using Microsoft.Extensions.Configuration; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Microsoft.Extensions.Hosting; 12 | 13 | namespace Test 14 | { 15 | public class Startup 16 | { 17 | public Startup(IConfiguration configuration) 18 | { 19 | Configuration = configuration; 20 | } 21 | 22 | public IConfiguration Configuration { get; } 23 | 24 | // This method gets called by the runtime. Use this method to add services to the container. 25 | // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 26 | public void ConfigureServices(IServiceCollection services) 27 | { 28 | services.AddRazorPages(); 29 | services.AddServerSideBlazor(); 30 | } 31 | 32 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 33 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 34 | { 35 | if (env.IsDevelopment()) 36 | { 37 | app.UseDeveloperExceptionPage(); 38 | } 39 | else 40 | { 41 | app.UseExceptionHandler("/Error"); 42 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 43 | app.UseHsts(); 44 | } 45 | 46 | app.UseHttpsRedirection(); 47 | app.UseStaticFiles(); 48 | 49 | app.UseRouting(); 50 | 51 | app.UseEndpoints(endpoints => 52 | { 53 | endpoints.MapBlazorHub(); 54 | endpoints.MapFallbackToPage("/_Host"); 55 | }); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Test/Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | net5.0 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Test/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using Microsoft.AspNetCore.Authorization 3 | @using Microsoft.AspNetCore.Components.Authorization 4 | @using Microsoft.AspNetCore.Components.Forms 5 | @using Microsoft.AspNetCore.Components.Routing 6 | @using Microsoft.AspNetCore.Components.Web 7 | @using Microsoft.AspNetCore.Components.Web.Virtualization 8 | @using Microsoft.JSInterop 9 | @using Test 10 | @using Test.Shared 11 | @using Test.Data 12 | -------------------------------------------------------------------------------- /Test/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "DetailedErrors": true, 3 | "Logging": { 4 | "LogLevel": { 5 | "Default": "Information", 6 | "Microsoft": "Warning", 7 | "Microsoft.Hosting.Lifetime": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Test/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /Test/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qkmaxware/Blazor-NodeEditor/85050293507e551483006cdd05b6323a4906217d/Test/preview.png -------------------------------------------------------------------------------- /Test/wwwroot/css/open-iconic/FONT-LICENSE: -------------------------------------------------------------------------------- 1 | SIL OPEN FONT LICENSE Version 1.1 2 | 3 | Copyright (c) 2014 Waybury 4 | 5 | PREAMBLE 6 | The goals of the Open Font License (OFL) are to stimulate worldwide 7 | development of collaborative font projects, to support the font creation 8 | efforts of academic and linguistic communities, and to provide a free and 9 | open framework in which fonts may be shared and improved in partnership 10 | with others. 11 | 12 | The OFL allows the licensed fonts to be used, studied, modified and 13 | redistributed freely as long as they are not sold by themselves. The 14 | fonts, including any derivative works, can be bundled, embedded, 15 | redistributed and/or sold with any software provided that any reserved 16 | names are not used by derivative works. The fonts and derivatives, 17 | however, cannot be released under any other type of license. The 18 | requirement for fonts to remain under this license does not apply 19 | to any document created using the fonts or their derivatives. 20 | 21 | DEFINITIONS 22 | "Font Software" refers to the set of files released by the Copyright 23 | Holder(s) under this license and clearly marked as such. This may 24 | include source files, build scripts and documentation. 25 | 26 | "Reserved Font Name" refers to any names specified as such after the 27 | copyright statement(s). 28 | 29 | "Original Version" refers to the collection of Font Software components as 30 | distributed by the Copyright Holder(s). 31 | 32 | "Modified Version" refers to any derivative made by adding to, deleting, 33 | or substituting -- in part or in whole -- any of the components of the 34 | Original Version, by changing formats or by porting the Font Software to a 35 | new environment. 36 | 37 | "Author" refers to any designer, engineer, programmer, technical 38 | writer or other person who contributed to the Font Software. 39 | 40 | PERMISSION & CONDITIONS 41 | Permission is hereby granted, free of charge, to any person obtaining 42 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 43 | redistribute, and sell modified and unmodified copies of the Font 44 | Software, subject to the following conditions: 45 | 46 | 1) Neither the Font Software nor any of its individual components, 47 | in Original or Modified Versions, may be sold by itself. 48 | 49 | 2) Original or Modified Versions of the Font Software may be bundled, 50 | redistributed and/or sold with any software, provided that each copy 51 | contains the above copyright notice and this license. These can be 52 | included either as stand-alone text files, human-readable headers or 53 | in the appropriate machine-readable metadata fields within text or 54 | binary files as long as those fields can be easily viewed by the user. 55 | 56 | 3) No Modified Version of the Font Software may use the Reserved Font 57 | Name(s) unless explicit written permission is granted by the corresponding 58 | Copyright Holder. This restriction only applies to the primary font name as 59 | presented to the users. 60 | 61 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 62 | Software shall not be used to promote, endorse or advertise any 63 | Modified Version, except to acknowledge the contribution(s) of the 64 | Copyright Holder(s) and the Author(s) or with their explicit written 65 | permission. 66 | 67 | 5) The Font Software, modified or unmodified, in part or in whole, 68 | must be distributed entirely under this license, and must not be 69 | distributed under any other license. The requirement for fonts to 70 | remain under this license does not apply to any document created 71 | using the Font Software. 72 | 73 | TERMINATION 74 | This license becomes null and void if any of the above conditions are 75 | not met. 76 | 77 | DISCLAIMER 78 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 79 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 80 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 81 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 82 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 83 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 84 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 85 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 86 | OTHER DEALINGS IN THE FONT SOFTWARE. 87 | -------------------------------------------------------------------------------- /Test/wwwroot/css/open-iconic/ICON-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Waybury 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /Test/wwwroot/css/open-iconic/README.md: -------------------------------------------------------------------------------- 1 | [Open Iconic v1.1.1](http://useiconic.com/open) 2 | =========== 3 | 4 | ### Open Iconic is the open source sibling of [Iconic](http://useiconic.com). It is a hyper-legible collection of 223 icons with a tiny footprint—ready to use with Bootstrap and Foundation. [View the collection](http://useiconic.com/open#icons) 5 | 6 | 7 | 8 | ## What's in Open Iconic? 9 | 10 | * 223 icons designed to be legible down to 8 pixels 11 | * Super-light SVG files - 61.8 for the entire set 12 | * SVG sprite—the modern replacement for icon fonts 13 | * Webfont (EOT, OTF, SVG, TTF, WOFF), PNG and WebP formats 14 | * Webfont stylesheets (including versions for Bootstrap and Foundation) in CSS, LESS, SCSS and Stylus formats 15 | * PNG and WebP raster images in 8px, 16px, 24px, 32px, 48px and 64px. 16 | 17 | 18 | ## Getting Started 19 | 20 | #### For code samples and everything else you need to get started with Open Iconic, check out our [Icons](http://useiconic.com/open#icons) and [Reference](http://useiconic.com/open#reference) sections. 21 | 22 | ### General Usage 23 | 24 | #### Using Open Iconic's SVGs 25 | 26 | We like SVGs and we think they're the way to display icons on the web. Since Open Iconic are just basic SVGs, we suggest you display them like you would any other image (don't forget the `alt` attribute). 27 | 28 | ``` 29 | icon name 30 | ``` 31 | 32 | #### Using Open Iconic's SVG Sprite 33 | 34 | Open Iconic also comes in a SVG sprite which allows you to display all the icons in the set with a single request. It's like an icon font, without being a hack. 35 | 36 | Adding an icon from an SVG sprite is a little different than what you're used to, but it's still a piece of cake. *Tip: To make your icons easily style able, we suggest adding a general class to the* `` *tag and a unique class name for each different icon in the* `` *tag.* 37 | 38 | ``` 39 | 40 | 41 | 42 | ``` 43 | 44 | Sizing icons only needs basic CSS. All the icons are in a square format, so just set the `` tag with equal width and height dimensions. 45 | 46 | ``` 47 | .icon { 48 | width: 16px; 49 | height: 16px; 50 | } 51 | ``` 52 | 53 | Coloring icons is even easier. All you need to do is set the `fill` rule on the `` tag. 54 | 55 | ``` 56 | .icon-account-login { 57 | fill: #f00; 58 | } 59 | ``` 60 | 61 | To learn more about SVG Sprites, read [Chris Coyier's guide](http://css-tricks.com/svg-sprites-use-better-icon-fonts/). 62 | 63 | #### Using Open Iconic's Icon Font... 64 | 65 | 66 | ##### …with Bootstrap 67 | 68 | You can find our Bootstrap stylesheets in `font/css/open-iconic-bootstrap.{css, less, scss, styl}` 69 | 70 | 71 | ``` 72 | 73 | ``` 74 | 75 | 76 | ``` 77 | 78 | ``` 79 | 80 | ##### …with Foundation 81 | 82 | You can find our Foundation stylesheets in `font/css/open-iconic-foundation.{css, less, scss, styl}` 83 | 84 | ``` 85 | 86 | ``` 87 | 88 | 89 | ``` 90 | 91 | ``` 92 | 93 | ##### …on its own 94 | 95 | You can find our default stylesheets in `font/css/open-iconic.{css, less, scss, styl}` 96 | 97 | ``` 98 | 99 | ``` 100 | 101 | ``` 102 | 103 | ``` 104 | 105 | 106 | ## License 107 | 108 | ### Icons 109 | 110 | All code (including SVG markup) is under the [MIT License](http://opensource.org/licenses/MIT). 111 | 112 | ### Fonts 113 | 114 | All fonts are under the [SIL Licensed](http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web). 115 | -------------------------------------------------------------------------------- /Test/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css: -------------------------------------------------------------------------------- 1 | @font-face{font-family:Icons;src:url(../fonts/open-iconic.eot);src:url(../fonts/open-iconic.eot?#iconic-sm) format('embedded-opentype'),url(../fonts/open-iconic.woff) format('woff'),url(../fonts/open-iconic.ttf) format('truetype'),url(../fonts/open-iconic.otf) format('opentype'),url(../fonts/open-iconic.svg#iconic-sm) format('svg');font-weight:400;font-style:normal}.oi{position:relative;top:1px;display:inline-block;speak:none;font-family:Icons;font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.oi:empty:before{width:1em;text-align:center;box-sizing:content-box}.oi.oi-align-center:before{text-align:center}.oi.oi-align-left:before{text-align:left}.oi.oi-align-right:before{text-align:right}.oi.oi-flip-horizontal:before{-webkit-transform:scale(-1,1);-ms-transform:scale(-1,1);transform:scale(-1,1)}.oi.oi-flip-vertical:before{-webkit-transform:scale(1,-1);-ms-transform:scale(-1,1);transform:scale(1,-1)}.oi.oi-flip-horizontal-vertical:before{-webkit-transform:scale(-1,-1);-ms-transform:scale(-1,1);transform:scale(-1,-1)}.oi-account-login:before{content:'\e000'}.oi-account-logout:before{content:'\e001'}.oi-action-redo:before{content:'\e002'}.oi-action-undo:before{content:'\e003'}.oi-align-center:before{content:'\e004'}.oi-align-left:before{content:'\e005'}.oi-align-right:before{content:'\e006'}.oi-aperture:before{content:'\e007'}.oi-arrow-bottom:before{content:'\e008'}.oi-arrow-circle-bottom:before{content:'\e009'}.oi-arrow-circle-left:before{content:'\e00a'}.oi-arrow-circle-right:before{content:'\e00b'}.oi-arrow-circle-top:before{content:'\e00c'}.oi-arrow-left:before{content:'\e00d'}.oi-arrow-right:before{content:'\e00e'}.oi-arrow-thick-bottom:before{content:'\e00f'}.oi-arrow-thick-left:before{content:'\e010'}.oi-arrow-thick-right:before{content:'\e011'}.oi-arrow-thick-top:before{content:'\e012'}.oi-arrow-top:before{content:'\e013'}.oi-audio-spectrum:before{content:'\e014'}.oi-audio:before{content:'\e015'}.oi-badge:before{content:'\e016'}.oi-ban:before{content:'\e017'}.oi-bar-chart:before{content:'\e018'}.oi-basket:before{content:'\e019'}.oi-battery-empty:before{content:'\e01a'}.oi-battery-full:before{content:'\e01b'}.oi-beaker:before{content:'\e01c'}.oi-bell:before{content:'\e01d'}.oi-bluetooth:before{content:'\e01e'}.oi-bold:before{content:'\e01f'}.oi-bolt:before{content:'\e020'}.oi-book:before{content:'\e021'}.oi-bookmark:before{content:'\e022'}.oi-box:before{content:'\e023'}.oi-briefcase:before{content:'\e024'}.oi-british-pound:before{content:'\e025'}.oi-browser:before{content:'\e026'}.oi-brush:before{content:'\e027'}.oi-bug:before{content:'\e028'}.oi-bullhorn:before{content:'\e029'}.oi-calculator:before{content:'\e02a'}.oi-calendar:before{content:'\e02b'}.oi-camera-slr:before{content:'\e02c'}.oi-caret-bottom:before{content:'\e02d'}.oi-caret-left:before{content:'\e02e'}.oi-caret-right:before{content:'\e02f'}.oi-caret-top:before{content:'\e030'}.oi-cart:before{content:'\e031'}.oi-chat:before{content:'\e032'}.oi-check:before{content:'\e033'}.oi-chevron-bottom:before{content:'\e034'}.oi-chevron-left:before{content:'\e035'}.oi-chevron-right:before{content:'\e036'}.oi-chevron-top:before{content:'\e037'}.oi-circle-check:before{content:'\e038'}.oi-circle-x:before{content:'\e039'}.oi-clipboard:before{content:'\e03a'}.oi-clock:before{content:'\e03b'}.oi-cloud-download:before{content:'\e03c'}.oi-cloud-upload:before{content:'\e03d'}.oi-cloud:before{content:'\e03e'}.oi-cloudy:before{content:'\e03f'}.oi-code:before{content:'\e040'}.oi-cog:before{content:'\e041'}.oi-collapse-down:before{content:'\e042'}.oi-collapse-left:before{content:'\e043'}.oi-collapse-right:before{content:'\e044'}.oi-collapse-up:before{content:'\e045'}.oi-command:before{content:'\e046'}.oi-comment-square:before{content:'\e047'}.oi-compass:before{content:'\e048'}.oi-contrast:before{content:'\e049'}.oi-copywriting:before{content:'\e04a'}.oi-credit-card:before{content:'\e04b'}.oi-crop:before{content:'\e04c'}.oi-dashboard:before{content:'\e04d'}.oi-data-transfer-download:before{content:'\e04e'}.oi-data-transfer-upload:before{content:'\e04f'}.oi-delete:before{content:'\e050'}.oi-dial:before{content:'\e051'}.oi-document:before{content:'\e052'}.oi-dollar:before{content:'\e053'}.oi-double-quote-sans-left:before{content:'\e054'}.oi-double-quote-sans-right:before{content:'\e055'}.oi-double-quote-serif-left:before{content:'\e056'}.oi-double-quote-serif-right:before{content:'\e057'}.oi-droplet:before{content:'\e058'}.oi-eject:before{content:'\e059'}.oi-elevator:before{content:'\e05a'}.oi-ellipses:before{content:'\e05b'}.oi-envelope-closed:before{content:'\e05c'}.oi-envelope-open:before{content:'\e05d'}.oi-euro:before{content:'\e05e'}.oi-excerpt:before{content:'\e05f'}.oi-expand-down:before{content:'\e060'}.oi-expand-left:before{content:'\e061'}.oi-expand-right:before{content:'\e062'}.oi-expand-up:before{content:'\e063'}.oi-external-link:before{content:'\e064'}.oi-eye:before{content:'\e065'}.oi-eyedropper:before{content:'\e066'}.oi-file:before{content:'\e067'}.oi-fire:before{content:'\e068'}.oi-flag:before{content:'\e069'}.oi-flash:before{content:'\e06a'}.oi-folder:before{content:'\e06b'}.oi-fork:before{content:'\e06c'}.oi-fullscreen-enter:before{content:'\e06d'}.oi-fullscreen-exit:before{content:'\e06e'}.oi-globe:before{content:'\e06f'}.oi-graph:before{content:'\e070'}.oi-grid-four-up:before{content:'\e071'}.oi-grid-three-up:before{content:'\e072'}.oi-grid-two-up:before{content:'\e073'}.oi-hard-drive:before{content:'\e074'}.oi-header:before{content:'\e075'}.oi-headphones:before{content:'\e076'}.oi-heart:before{content:'\e077'}.oi-home:before{content:'\e078'}.oi-image:before{content:'\e079'}.oi-inbox:before{content:'\e07a'}.oi-infinity:before{content:'\e07b'}.oi-info:before{content:'\e07c'}.oi-italic:before{content:'\e07d'}.oi-justify-center:before{content:'\e07e'}.oi-justify-left:before{content:'\e07f'}.oi-justify-right:before{content:'\e080'}.oi-key:before{content:'\e081'}.oi-laptop:before{content:'\e082'}.oi-layers:before{content:'\e083'}.oi-lightbulb:before{content:'\e084'}.oi-link-broken:before{content:'\e085'}.oi-link-intact:before{content:'\e086'}.oi-list-rich:before{content:'\e087'}.oi-list:before{content:'\e088'}.oi-location:before{content:'\e089'}.oi-lock-locked:before{content:'\e08a'}.oi-lock-unlocked:before{content:'\e08b'}.oi-loop-circular:before{content:'\e08c'}.oi-loop-square:before{content:'\e08d'}.oi-loop:before{content:'\e08e'}.oi-magnifying-glass:before{content:'\e08f'}.oi-map-marker:before{content:'\e090'}.oi-map:before{content:'\e091'}.oi-media-pause:before{content:'\e092'}.oi-media-play:before{content:'\e093'}.oi-media-record:before{content:'\e094'}.oi-media-skip-backward:before{content:'\e095'}.oi-media-skip-forward:before{content:'\e096'}.oi-media-step-backward:before{content:'\e097'}.oi-media-step-forward:before{content:'\e098'}.oi-media-stop:before{content:'\e099'}.oi-medical-cross:before{content:'\e09a'}.oi-menu:before{content:'\e09b'}.oi-microphone:before{content:'\e09c'}.oi-minus:before{content:'\e09d'}.oi-monitor:before{content:'\e09e'}.oi-moon:before{content:'\e09f'}.oi-move:before{content:'\e0a0'}.oi-musical-note:before{content:'\e0a1'}.oi-paperclip:before{content:'\e0a2'}.oi-pencil:before{content:'\e0a3'}.oi-people:before{content:'\e0a4'}.oi-person:before{content:'\e0a5'}.oi-phone:before{content:'\e0a6'}.oi-pie-chart:before{content:'\e0a7'}.oi-pin:before{content:'\e0a8'}.oi-play-circle:before{content:'\e0a9'}.oi-plus:before{content:'\e0aa'}.oi-power-standby:before{content:'\e0ab'}.oi-print:before{content:'\e0ac'}.oi-project:before{content:'\e0ad'}.oi-pulse:before{content:'\e0ae'}.oi-puzzle-piece:before{content:'\e0af'}.oi-question-mark:before{content:'\e0b0'}.oi-rain:before{content:'\e0b1'}.oi-random:before{content:'\e0b2'}.oi-reload:before{content:'\e0b3'}.oi-resize-both:before{content:'\e0b4'}.oi-resize-height:before{content:'\e0b5'}.oi-resize-width:before{content:'\e0b6'}.oi-rss-alt:before{content:'\e0b7'}.oi-rss:before{content:'\e0b8'}.oi-script:before{content:'\e0b9'}.oi-share-boxed:before{content:'\e0ba'}.oi-share:before{content:'\e0bb'}.oi-shield:before{content:'\e0bc'}.oi-signal:before{content:'\e0bd'}.oi-signpost:before{content:'\e0be'}.oi-sort-ascending:before{content:'\e0bf'}.oi-sort-descending:before{content:'\e0c0'}.oi-spreadsheet:before{content:'\e0c1'}.oi-star:before{content:'\e0c2'}.oi-sun:before{content:'\e0c3'}.oi-tablet:before{content:'\e0c4'}.oi-tag:before{content:'\e0c5'}.oi-tags:before{content:'\e0c6'}.oi-target:before{content:'\e0c7'}.oi-task:before{content:'\e0c8'}.oi-terminal:before{content:'\e0c9'}.oi-text:before{content:'\e0ca'}.oi-thumb-down:before{content:'\e0cb'}.oi-thumb-up:before{content:'\e0cc'}.oi-timer:before{content:'\e0cd'}.oi-transfer:before{content:'\e0ce'}.oi-trash:before{content:'\e0cf'}.oi-underline:before{content:'\e0d0'}.oi-vertical-align-bottom:before{content:'\e0d1'}.oi-vertical-align-center:before{content:'\e0d2'}.oi-vertical-align-top:before{content:'\e0d3'}.oi-video:before{content:'\e0d4'}.oi-volume-high:before{content:'\e0d5'}.oi-volume-low:before{content:'\e0d6'}.oi-volume-off:before{content:'\e0d7'}.oi-warning:before{content:'\e0d8'}.oi-wifi:before{content:'\e0d9'}.oi-wrench:before{content:'\e0da'}.oi-x:before{content:'\e0db'}.oi-yen:before{content:'\e0dc'}.oi-zoom-in:before{content:'\e0dd'}.oi-zoom-out:before{content:'\e0de'} -------------------------------------------------------------------------------- /Test/wwwroot/css/open-iconic/font/fonts/open-iconic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qkmaxware/Blazor-NodeEditor/85050293507e551483006cdd05b6323a4906217d/Test/wwwroot/css/open-iconic/font/fonts/open-iconic.eot -------------------------------------------------------------------------------- /Test/wwwroot/css/open-iconic/font/fonts/open-iconic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qkmaxware/Blazor-NodeEditor/85050293507e551483006cdd05b6323a4906217d/Test/wwwroot/css/open-iconic/font/fonts/open-iconic.otf -------------------------------------------------------------------------------- /Test/wwwroot/css/open-iconic/font/fonts/open-iconic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Created by FontForge 20120731 at Tue Jul 1 20:39:22 2014 9 | By P.J. Onori 10 | Created by P.J. Onori with FontForge 2.0 (http://fontforge.sf.net) 11 | 12 | 13 | 14 | 27 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 45 | 47 | 49 | 51 | 53 | 55 | 57 | 59 | 61 | 63 | 65 | 67 | 69 | 71 | 74 | 76 | 79 | 81 | 84 | 86 | 88 | 91 | 93 | 95 | 98 | 100 | 102 | 104 | 106 | 109 | 112 | 115 | 117 | 121 | 123 | 125 | 127 | 130 | 132 | 134 | 136 | 138 | 141 | 143 | 145 | 147 | 149 | 151 | 153 | 155 | 157 | 159 | 162 | 165 | 167 | 169 | 172 | 174 | 177 | 179 | 181 | 183 | 185 | 189 | 191 | 194 | 196 | 198 | 200 | 202 | 205 | 207 | 209 | 211 | 213 | 215 | 218 | 220 | 222 | 224 | 226 | 228 | 230 | 232 | 234 | 236 | 238 | 241 | 243 | 245 | 247 | 249 | 251 | 253 | 256 | 259 | 261 | 263 | 265 | 267 | 269 | 272 | 274 | 276 | 280 | 282 | 285 | 287 | 289 | 292 | 295 | 298 | 300 | 302 | 304 | 306 | 309 | 312 | 314 | 316 | 318 | 320 | 322 | 324 | 326 | 330 | 334 | 338 | 340 | 343 | 345 | 347 | 349 | 351 | 353 | 355 | 358 | 360 | 363 | 365 | 367 | 369 | 371 | 373 | 375 | 377 | 379 | 381 | 383 | 386 | 388 | 390 | 392 | 394 | 396 | 399 | 401 | 404 | 406 | 408 | 410 | 412 | 414 | 416 | 419 | 421 | 423 | 425 | 428 | 431 | 435 | 438 | 440 | 442 | 444 | 446 | 448 | 451 | 453 | 455 | 457 | 460 | 462 | 464 | 466 | 468 | 471 | 473 | 477 | 479 | 481 | 483 | 486 | 488 | 490 | 492 | 494 | 496 | 499 | 501 | 504 | 506 | 509 | 512 | 515 | 517 | 520 | 522 | 524 | 526 | 529 | 532 | 534 | 536 | 539 | 542 | 543 | 544 | -------------------------------------------------------------------------------- /Test/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qkmaxware/Blazor-NodeEditor/85050293507e551483006cdd05b6323a4906217d/Test/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf -------------------------------------------------------------------------------- /Test/wwwroot/css/open-iconic/font/fonts/open-iconic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qkmaxware/Blazor-NodeEditor/85050293507e551483006cdd05b6323a4906217d/Test/wwwroot/css/open-iconic/font/fonts/open-iconic.woff -------------------------------------------------------------------------------- /Test/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | @import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); 2 | 3 | html, body { 4 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 5 | } 6 | 7 | a, .btn-link { 8 | color: #0366d6; 9 | } 10 | 11 | .btn-primary { 12 | color: #fff; 13 | background-color: #1b6ec2; 14 | border-color: #1861ac; 15 | } 16 | 17 | .content { 18 | padding-top: 1.1rem; 19 | } 20 | 21 | .valid.modified:not([type=checkbox]) { 22 | outline: 1px solid #26b050; 23 | } 24 | 25 | .invalid { 26 | outline: 1px solid red; 27 | } 28 | 29 | .validation-message { 30 | color: red; 31 | } 32 | 33 | #blazor-error-ui { 34 | background: lightyellow; 35 | bottom: 0; 36 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 37 | display: none; 38 | left: 0; 39 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 40 | position: fixed; 41 | width: 100%; 42 | z-index: 1000; 43 | } 44 | 45 | #blazor-error-ui .dismiss { 46 | cursor: pointer; 47 | position: absolute; 48 | right: 0.75rem; 49 | top: 0.5rem; 50 | } 51 | -------------------------------------------------------------------------------- /Test/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qkmaxware/Blazor-NodeEditor/85050293507e551483006cdd05b6323a4906217d/Test/wwwroot/favicon.ico -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | --------------------------------------------------------------------------------