mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-13 05:48:35 +00:00
Merge branch 'feature/NodeSorting' into development
This commit is contained in:
commit
d7c8039a37
@ -93,6 +93,7 @@
|
|||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=stores_005Cregistrations/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=stores_005Cregistrations/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=utilities/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=utilities/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=visualscripting/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=visualscripting/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=visualscripting_005Cextensions/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=visualscripting_005Cinterfaces/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=visualscripting_005Cinterfaces/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=visualscripting_005Cnodes/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=visualscripting_005Cnodes/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=visualscripting_005Cpins/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=visualscripting_005Cpins/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||||
88
src/Artemis.Core/VisualScripting/Extensions/NodeExtension.cs
Normal file
88
src/Artemis.Core/VisualScripting/Extensions/NodeExtension.cs
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Artemis.Core;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides extension methods for nodes.
|
||||||
|
/// </summary>
|
||||||
|
public static class NodeExtension
|
||||||
|
{
|
||||||
|
#region Methods
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Estimates a height of a node in the editor.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="node">The node whose height to estimate.</param>
|
||||||
|
/// <returns>The estimated height in pixels.</returns>
|
||||||
|
public static double EstimateHeight(this INode node)
|
||||||
|
{
|
||||||
|
const double PIN_HEIGHT = 26;
|
||||||
|
const double TITLE_HEIGHT = 46;
|
||||||
|
|
||||||
|
int inputPinCount = node.Pins.Count(x => x.Direction == PinDirection.Input)
|
||||||
|
+ node.PinCollections.Where(x => x.Direction == PinDirection.Input).Sum(x => x.Count() + 1);
|
||||||
|
int outputPinCount = node.Pins.Count(x => x.Direction == PinDirection.Output)
|
||||||
|
+ node.PinCollections.Where(x => x.Direction == PinDirection.Output).Sum(x => x.Count() + 1);
|
||||||
|
|
||||||
|
return TITLE_HEIGHT + Math.Max(inputPinCount, outputPinCount) * PIN_HEIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Estimates a width a node in the editor.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="node">The node whose width to estimate.</param>
|
||||||
|
/// <returns>The estimated width in pixels.</returns>
|
||||||
|
public static double EstimateWidth(this INode node)
|
||||||
|
{
|
||||||
|
// DarthAffe 13.09.2022: For now just assume they are all the same size
|
||||||
|
return 120;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether the node is part of a loop when the provided pending connecting would be connected.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="node">The node to check</param>
|
||||||
|
/// <param name="pendingConnection">The node to which a connection is pending</param>
|
||||||
|
/// <returns><see langword="true" /> if there would be a loop; otherwise <see langword="false" />.</returns>
|
||||||
|
public static bool IsInLoop(this INode node, INode pendingConnection)
|
||||||
|
{
|
||||||
|
HashSet<INode> checkedNodes = new();
|
||||||
|
|
||||||
|
bool CheckNode(INode checkNode, INode? pending)
|
||||||
|
{
|
||||||
|
if (checkedNodes.Contains(checkNode)) return false;
|
||||||
|
|
||||||
|
checkedNodes.Add(checkNode);
|
||||||
|
|
||||||
|
List<INode> connectedNodes = checkNode.Pins
|
||||||
|
.Where(x => x.Direction == PinDirection.Input)
|
||||||
|
.SelectMany(x => x.ConnectedTo)
|
||||||
|
.Select(x => x.Node)
|
||||||
|
.Concat(checkNode.PinCollections
|
||||||
|
.Where(x => x.Direction == PinDirection.Input)
|
||||||
|
.SelectMany(x => x)
|
||||||
|
.SelectMany(x => x.ConnectedTo)
|
||||||
|
.Select(x => x.Node))
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
if (pending != null)
|
||||||
|
connectedNodes.Add(pending);
|
||||||
|
|
||||||
|
foreach (INode connectedNode in connectedNodes)
|
||||||
|
{
|
||||||
|
if (connectedNode == node)
|
||||||
|
return true;
|
||||||
|
else if (CheckNode(connectedNode, null))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CheckNode(node, pendingConnection);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@ -0,0 +1,97 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Artemis.Core;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides extension methods for node scripts.
|
||||||
|
/// </summary>
|
||||||
|
public static class NodeScriptExtension
|
||||||
|
{
|
||||||
|
#region Methods
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Organize a node script, attempting to lay out nodes in a logical manner.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="nodeScript">The node script to organize.</param>
|
||||||
|
public static void Organize(this NodeScript nodeScript)
|
||||||
|
{
|
||||||
|
const double SPACING_HORIZONTAL = 160;
|
||||||
|
const double SPACING_VERTICAL = 20;
|
||||||
|
|
||||||
|
Dictionary<INode, int> levels = nodeScript.Nodes.ToDictionary(node => node, _ => -1);
|
||||||
|
|
||||||
|
List<INode> currentLevelNodes = nodeScript.Nodes.Where(x => x.IsExitNode).ToList();
|
||||||
|
foreach (INode currentLevelNode in currentLevelNodes)
|
||||||
|
levels[currentLevelNode] = 0; // DarthAffe 13.09.2022: Init-exit nodes as zero
|
||||||
|
|
||||||
|
int currentLevel = 1;
|
||||||
|
while (currentLevelNodes.Count > 0 && currentLevel < 1000)
|
||||||
|
{
|
||||||
|
List<INode> nextLevelNodes = currentLevelNodes.SelectMany(node => node.Pins
|
||||||
|
.Where(x => x.Direction == PinDirection.Input)
|
||||||
|
.SelectMany(x => x.ConnectedTo)
|
||||||
|
.Select(x => x.Node)
|
||||||
|
.Concat(node.PinCollections
|
||||||
|
.Where(x => x.Direction == PinDirection.Input)
|
||||||
|
.SelectMany(x => x)
|
||||||
|
.SelectMany(x => x.ConnectedTo)
|
||||||
|
.Select(x => x.Node)))
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (INode nextLevelNode in nextLevelNodes)
|
||||||
|
{
|
||||||
|
if (currentLevel > levels[nextLevelNode])
|
||||||
|
levels[nextLevelNode] = currentLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentLevelNodes = nextLevelNodes;
|
||||||
|
currentLevel++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LayoutLevel(IList<INode> nodes, double posX, double posY)
|
||||||
|
{
|
||||||
|
foreach (INode node in nodes)
|
||||||
|
{
|
||||||
|
node.X = posX;
|
||||||
|
node.Y = posY;
|
||||||
|
|
||||||
|
posY += SPACING_VERTICAL + node.EstimateHeight();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<INode>? unusedNodes = null;
|
||||||
|
double unusedPosY = 0;
|
||||||
|
double level0Width = 0;
|
||||||
|
|
||||||
|
double positionX = 0;
|
||||||
|
foreach (IGrouping<int, INode> levelGroup in levels.GroupBy(x => x.Value, x => x.Key).OrderBy(x => x.Key))
|
||||||
|
{
|
||||||
|
List<INode> nodes = levelGroup.ToList();
|
||||||
|
double levelHeight = nodes.Sum(x => x.EstimateHeight()) + (nodes.Count - 1) * SPACING_VERTICAL;
|
||||||
|
double levelWidth = nodes.Max(x => x.EstimateWidth());
|
||||||
|
double positionY = -(levelHeight / 2.0);
|
||||||
|
|
||||||
|
if (levelGroup.Key == -1)
|
||||||
|
{
|
||||||
|
unusedNodes = nodes;
|
||||||
|
unusedPosY = positionY;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (levelGroup.Key == 0)
|
||||||
|
level0Width = levelWidth;
|
||||||
|
|
||||||
|
LayoutLevel(nodes, positionX, positionY);
|
||||||
|
|
||||||
|
positionX -= SPACING_HORIZONTAL + levelWidth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unusedNodes != null)
|
||||||
|
LayoutLevel(unusedNodes, level0Width + SPACING_HORIZONTAL / 2.0, unusedPosY);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Artemis.Core;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Shared.Services.NodeEditor.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a node editor command that can be used to organize a script
|
||||||
|
/// </summary>
|
||||||
|
public class OrganizeScript : INodeEditorCommand
|
||||||
|
{
|
||||||
|
private readonly NodeScript _script;
|
||||||
|
private readonly List<(INode node, double x, double y)> _originalPositions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new instance of the <see cref="OrganizeScript"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="script">The script to organize.</param>
|
||||||
|
public OrganizeScript(NodeScript script)
|
||||||
|
{
|
||||||
|
_script = script;
|
||||||
|
_originalPositions = script.Nodes.Select(n => (n, n.X, n.Y)).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string DisplayName => "Organize script";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Execute()
|
||||||
|
{
|
||||||
|
_script.Organize();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Undo()
|
||||||
|
{
|
||||||
|
foreach ((INode? node, double x, double y) in _originalPositions)
|
||||||
|
{
|
||||||
|
node.X = x;
|
||||||
|
node.Y = y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reactive;
|
using System.Reactive;
|
||||||
using System.Reactive.Linq;
|
using System.Reactive.Linq;
|
||||||
@ -197,8 +198,14 @@ public class NodeScriptViewModel : ActivatableViewModelBase
|
|||||||
DragViewModel = new DragCableViewModel(sourcePinViewModel);
|
DragViewModel = new DragCableViewModel(sourcePinViewModel);
|
||||||
|
|
||||||
DragViewModel.DragPoint = position;
|
DragViewModel.DragPoint = position;
|
||||||
|
if (targetPinVmModel == null)
|
||||||
|
return true;
|
||||||
|
if (!targetPinVmModel.IsCompatibleWith(sourcePinViewModel))
|
||||||
|
return false;
|
||||||
|
|
||||||
return targetPinVmModel == null || targetPinVmModel.IsCompatibleWith(sourcePinViewModel);
|
return sourcePinViewModel.Pin.Direction == PinDirection.Output
|
||||||
|
? !targetPinVmModel.Pin.Node.IsInLoop(sourcePinViewModel.Pin.Node)
|
||||||
|
: !sourcePinViewModel.Pin.Node.IsInLoop(targetPinVmModel.Pin.Node);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void FinishPinDrag(PinViewModel sourcePinViewModel, PinViewModel? targetPinVmModel, Point position)
|
public void FinishPinDrag(PinViewModel sourcePinViewModel, PinViewModel? targetPinVmModel, Point position)
|
||||||
@ -211,7 +218,10 @@ public class NodeScriptViewModel : ActivatableViewModelBase
|
|||||||
// If dropped on top of a compatible pin, connect to it
|
// If dropped on top of a compatible pin, connect to it
|
||||||
if (targetPinVmModel != null && targetPinVmModel.IsCompatibleWith(sourcePinViewModel))
|
if (targetPinVmModel != null && targetPinVmModel.IsCompatibleWith(sourcePinViewModel))
|
||||||
{
|
{
|
||||||
_nodeEditorService.ExecuteCommand(NodeScript, new ConnectPins(sourcePinViewModel.Pin, targetPinVmModel.Pin));
|
if (sourcePinViewModel.Pin.Direction == PinDirection.Output && !targetPinVmModel.Pin.Node.IsInLoop(sourcePinViewModel.Pin.Node))
|
||||||
|
_nodeEditorService.ExecuteCommand(NodeScript, new ConnectPins(sourcePinViewModel.Pin, targetPinVmModel.Pin));
|
||||||
|
else if (sourcePinViewModel.Pin.Direction == PinDirection.Input && !sourcePinViewModel.Pin.Node.IsInLoop(targetPinVmModel.Pin.Node))
|
||||||
|
_nodeEditorService.ExecuteCommand(NodeScript, new ConnectPins(sourcePinViewModel.Pin, targetPinVmModel.Pin));
|
||||||
}
|
}
|
||||||
// If not dropped on a pin allow the user to create a new node
|
// If not dropped on a pin allow the user to create a new node
|
||||||
else if (targetPinVmModel == null)
|
else if (targetPinVmModel == null)
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
<Window.KeyBindings>
|
<Window.KeyBindings>
|
||||||
<KeyBinding Command="{CompiledBinding History.Undo}" Gesture="Ctrl+Z" />
|
<KeyBinding Command="{CompiledBinding History.Undo}" Gesture="Ctrl+Z" />
|
||||||
<KeyBinding Command="{CompiledBinding History.Redo}" Gesture="Ctrl+Y" />
|
<KeyBinding Command="{CompiledBinding History.Redo}" Gesture="Ctrl+Y" />
|
||||||
|
<KeyBinding Command="{CompiledBinding AutoArrange}" Gesture="Ctrl+F" />
|
||||||
</Window.KeyBindings>
|
</Window.KeyBindings>
|
||||||
<Grid Margin="15" ColumnDefinitions="*,*" RowDefinitions="Auto,Auto,*">
|
<Grid Margin="15" ColumnDefinitions="*,*" RowDefinitions="Auto,Auto,*">
|
||||||
<Menu Grid.Row="0" Grid.ColumnSpan="2" VerticalAlignment="Top" Margin="-10 -10 -10 0">
|
<Menu Grid.Row="0" Grid.ColumnSpan="2" VerticalAlignment="Top" Margin="-10 -10 -10 0">
|
||||||
@ -43,6 +44,11 @@
|
|||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</MenuItem.DataTemplates>
|
</MenuItem.DataTemplates>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
<MenuItem Header="Auto-arrange" Command="{CompiledBinding AutoArrange}" InputGesture="Ctrl+F">
|
||||||
|
<MenuItem.Icon>
|
||||||
|
<avalonia:MaterialIcon Kind="Sort" />
|
||||||
|
</MenuItem.Icon>
|
||||||
|
</MenuItem>
|
||||||
<Separator />
|
<Separator />
|
||||||
<MenuItem Header="Export Script" Command="{CompiledBinding Export}">
|
<MenuItem Header="Export Script" Command="{CompiledBinding Export}">
|
||||||
<MenuItem.Icon>
|
<MenuItem.Icon>
|
||||||
|
|||||||
@ -26,6 +26,7 @@ public class NodeScriptWindowViewModel : NodeScriptWindowViewModelBase
|
|||||||
private readonly IProfileService _profileService;
|
private readonly IProfileService _profileService;
|
||||||
private readonly ISettingsService _settingsService;
|
private readonly ISettingsService _settingsService;
|
||||||
private readonly IWindowService _windowService;
|
private readonly IWindowService _windowService;
|
||||||
|
private bool _pauseUpdate;
|
||||||
|
|
||||||
public NodeScriptWindowViewModel(NodeScript nodeScript,
|
public NodeScriptWindowViewModel(NodeScript nodeScript,
|
||||||
INodeService nodeService,
|
INodeService nodeService,
|
||||||
@ -56,6 +57,7 @@ public class NodeScriptWindowViewModel : NodeScriptWindowViewModelBase
|
|||||||
Categories = categories;
|
Categories = categories;
|
||||||
|
|
||||||
CreateNode = ReactiveCommand.Create<NodeData>(ExecuteCreateNode);
|
CreateNode = ReactiveCommand.Create<NodeData>(ExecuteCreateNode);
|
||||||
|
AutoArrange = ReactiveCommand.CreateFromTask(ExecuteAutoArrange);
|
||||||
Export = ReactiveCommand.CreateFromTask(ExecuteExport);
|
Export = ReactiveCommand.CreateFromTask(ExecuteExport);
|
||||||
Import = ReactiveCommand.CreateFromTask(ExecuteImport);
|
Import = ReactiveCommand.CreateFromTask(ExecuteImport);
|
||||||
|
|
||||||
@ -83,6 +85,7 @@ public class NodeScriptWindowViewModel : NodeScriptWindowViewModelBase
|
|||||||
public ReactiveCommand<string, Unit> OpenUri { get; set; }
|
public ReactiveCommand<string, Unit> OpenUri { get; set; }
|
||||||
public ReadOnlyObservableCollection<IGrouping<NodeData, string>> Categories { get; }
|
public ReadOnlyObservableCollection<IGrouping<NodeData, string>> Categories { get; }
|
||||||
public ReactiveCommand<NodeData, Unit> CreateNode { get; }
|
public ReactiveCommand<NodeData, Unit> CreateNode { get; }
|
||||||
|
public ReactiveCommand<Unit, Unit> AutoArrange { get; }
|
||||||
public ReactiveCommand<Unit, Unit> Export { get; }
|
public ReactiveCommand<Unit, Unit> Export { get; }
|
||||||
public ReactiveCommand<Unit, Unit> Import { get; }
|
public ReactiveCommand<Unit, Unit> Import { get; }
|
||||||
|
|
||||||
@ -108,6 +111,27 @@ public class NodeScriptWindowViewModel : NodeScriptWindowViewModelBase
|
|||||||
_nodeEditorService.ExecuteCommand(NodeScript, new AddNode(NodeScript, node));
|
_nodeEditorService.ExecuteCommand(NodeScript, new AddNode(NodeScript, node));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ExecuteAutoArrange()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!NodeScript.ExitNodeConnected)
|
||||||
|
{
|
||||||
|
await _windowService.ShowConfirmContentDialog("Cannot auto-arrange", "The exit node must be connected in order to perform auto-arrange.", "Close", null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_pauseUpdate = true;
|
||||||
|
_nodeEditorService.ExecuteCommand(NodeScript, new OrganizeScript(NodeScript));
|
||||||
|
await Task.Delay(200);
|
||||||
|
NodeScriptViewModel.RequestAutoFit();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_pauseUpdate = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task ExecuteExport()
|
private async Task ExecuteExport()
|
||||||
{
|
{
|
||||||
// Might not cover everything but then the dialog will complain and that's good enough
|
// Might not cover everything but then the dialog will complain and that's good enough
|
||||||
@ -131,17 +155,26 @@ public class NodeScriptWindowViewModel : NodeScriptWindowViewModelBase
|
|||||||
if (result == null)
|
if (result == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
string json = await File.ReadAllTextAsync(result[0]);
|
try
|
||||||
_nodeService.ImportScript(json, NodeScript);
|
{
|
||||||
History.Clear();
|
_pauseUpdate = true;
|
||||||
|
string json = await File.ReadAllTextAsync(result[0]);
|
||||||
|
_nodeService.ImportScript(json, NodeScript);
|
||||||
|
History.Clear();
|
||||||
|
|
||||||
await Task.Delay(200);
|
await Task.Delay(200);
|
||||||
NodeScriptViewModel.RequestAutoFit();
|
NodeScriptViewModel.RequestAutoFit();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_pauseUpdate = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Update(object? sender, EventArgs e)
|
private void Update(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
NodeScript.Run();
|
if (!_pauseUpdate)
|
||||||
|
NodeScript.Run();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Save(object? sender, EventArgs e)
|
private void Save(object? sender, EventArgs e)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user