diff --git a/AXNode.png b/AXNode.png
new file mode 100644
index 0000000..ffffddd
Binary files /dev/null and b/AXNode.png differ
diff --git a/AXNode/AXNode.csproj b/AXNode/AXNode.csproj
new file mode 100644
index 0000000..09248ae
--- /dev/null
+++ b/AXNode/AXNode.csproj
@@ -0,0 +1,50 @@
+
+
+ WinExe
+ net8.0
+ enable
+ true
+ app.manifest
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AXNode/App.axaml b/AXNode/App.axaml
new file mode 100644
index 0000000..7e799ea
--- /dev/null
+++ b/AXNode/App.axaml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AXNode/App.axaml.cs b/AXNode/App.axaml.cs
new file mode 100644
index 0000000..1153e82
--- /dev/null
+++ b/AXNode/App.axaml.cs
@@ -0,0 +1,41 @@
+using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Markup.Xaml;
+using AXNode.AppTool;
+using XLib.Animate;
+
+namespace AXNode;
+
+public partial class App : Application
+{
+ public override void Initialize()
+ {
+ AvaloniaXamlLoader.Load(this);
+
+ // AnimationEngine.Instance.Start();
+ Init();
+ }
+
+ public override void OnFrameworkInitializationCompleted()
+ {
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ desktop.MainWindow = new MainWindow();
+ }
+
+ base.OnFrameworkInitializationCompleted();
+ }
+
+ private void Init()
+ {
+ // 初始化应用程序代理
+ AppDelegate.Init();
+
+ // 初始化系统数据
+ SystemDataDelegate.Instance.Init();
+ // 启动系统服务
+ SystemServiceDelegate.Instance.Start();
+ // 初始化系统工具
+ SystemToolDelegate.Instance.Init();
+ }
+}
\ No newline at end of file
diff --git a/AXNode/AppTool/AppDelegate.cs b/AXNode/AppTool/AppDelegate.cs
new file mode 100644
index 0000000..91cf154
--- /dev/null
+++ b/AXNode/AppTool/AppDelegate.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Windows;
+using Avalonia;
+using Avalonia.Threading;
+
+namespace AXNode.AppTool
+{
+ ///
+ /// 应用程序代理
+ ///
+ public class AppDelegate
+ {
+ public static App Main { get; set; }
+
+ public static string AppTitle => "XNode 1.0.3 Alpha";
+
+ public static void Init()
+ {
+ if (Application.Current is App app) Main = app;
+ }
+
+ public static void Invoke(Action action)
+ {
+ Dispatcher.UIThread.Invoke(action);
+ }
+
+ public static void BeginInvoke(Action action)
+ {
+ Dispatcher.UIThread.Invoke(action);
+ }
+ }
+}
\ No newline at end of file
diff --git a/AXNode/AppTool/ClassExtension.cs b/AXNode/AppTool/ClassExtension.cs
new file mode 100644
index 0000000..1bb3bc2
--- /dev/null
+++ b/AXNode/AppTool/ClassExtension.cs
@@ -0,0 +1,22 @@
+using XLib.Node;
+using AXNode.SubSystem.NodeEditSystem.Define;
+
+namespace AXNode.AppTool
+{
+ public static class ClassExtension
+ {
+ ///
+ /// 获取引脚路径
+ ///
+ public static PinPath GetPinPath(this PinBase pin)
+ {
+ return new PinPath
+ {
+ NodeVersion = pin.OwnerGroup.OwnerNode.Version,
+ NodeID = pin.OwnerGroup.OwnerNode.ID,
+ GroupIndex = pin.OwnerGroup.Index,
+ PinIndex = pin.OwnerGroup.GetPinIndex(pin)
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/AXNode/AppTool/ProjectDelegate.cs b/AXNode/AppTool/ProjectDelegate.cs
new file mode 100644
index 0000000..d8212d9
--- /dev/null
+++ b/AXNode/AppTool/ProjectDelegate.cs
@@ -0,0 +1,6 @@
+namespace AXNode.AppTool
+{
+ class ProjectDelegate
+ {
+ }
+}
\ No newline at end of file
diff --git a/AXNode/AppTool/SystemDelegate.cs b/AXNode/AppTool/SystemDelegate.cs
new file mode 100644
index 0000000..8546b82
--- /dev/null
+++ b/AXNode/AppTool/SystemDelegate.cs
@@ -0,0 +1,64 @@
+using XLib.Animate;
+using XLib.Base.AppFrame;
+using AXNode.SubSystem.CacheSystem;
+using AXNode.SubSystem.ControlSystem;
+using AXNode.SubSystem.EventSystem;
+using AXNode.SubSystem.NodeLibSystem;
+using AXNode.SubSystem.OptionSystem;
+using AXNode.SubSystem.ResourceSystem;
+using AXNode.SubSystem.TimerSystem;
+
+namespace AXNode.AppTool
+{
+ ///
+ /// 系统数据代理
+ ///
+ public class SystemDataDelegate : ManagerDelegate
+ {
+ private SystemDataDelegate()
+ {
+ // 选项、缓存
+ ManagerList.Add(OptionManager.Instance);
+ ManagerList.Add(CacheManager.Instance);
+ // 资源、节点库
+ ManagerList.Add(ResourceManager.Instance);
+ ManagerList.Add(NodeLibManager.Instance);
+ }
+
+ public static SystemDataDelegate Instance { get; } = new SystemDataDelegate();
+ }
+
+ ///
+ /// 系统服务代理
+ ///
+ public class SystemServiceDelegate : ServiceDelegate
+ {
+ private SystemServiceDelegate()
+ {
+ // 时间引擎
+ ServiceList.Add(TimeEngine.Instance);
+ // 应用定时器
+ ServiceList.Add(AppTimer.Instance);
+ // 控制引擎
+ ServiceList.Add(ControlEngine.Instance);
+ // 动画引擎
+ ServiceList.Add(AnimationEngine.Instance);
+ }
+
+ public static SystemServiceDelegate Instance { get; } = new SystemServiceDelegate();
+ }
+
+ ///
+ /// 系统工具代理
+ ///
+ public class SystemToolDelegate : ManagerDelegate
+ {
+ private SystemToolDelegate()
+ {
+ // 事件管理器
+ ManagerList.Add(EM.Instance);
+ }
+
+ public static SystemToolDelegate Instance { get; } = new SystemToolDelegate();
+ }
+}
\ No newline at end of file
diff --git a/AXNode/Assets/Cursor/CanNotMove.cur b/AXNode/Assets/Cursor/CanNotMove.cur
new file mode 100644
index 0000000..61f4add
Binary files /dev/null and b/AXNode/Assets/Cursor/CanNotMove.cur differ
diff --git a/AXNode/Assets/Cursor/Cross.cur b/AXNode/Assets/Cursor/Cross.cur
new file mode 100644
index 0000000..b7f63d6
Binary files /dev/null and b/AXNode/Assets/Cursor/Cross.cur differ
diff --git a/AXNode/Assets/Cursor/Disable.cur b/AXNode/Assets/Cursor/Disable.cur
new file mode 100644
index 0000000..b753684
Binary files /dev/null and b/AXNode/Assets/Cursor/Disable.cur differ
diff --git a/AXNode/Assets/Cursor/DragObject.cur b/AXNode/Assets/Cursor/DragObject.cur
new file mode 100644
index 0000000..b000e06
Binary files /dev/null and b/AXNode/Assets/Cursor/DragObject.cur differ
diff --git a/AXNode/Assets/Cursor/Draw.cur b/AXNode/Assets/Cursor/Draw.cur
new file mode 100644
index 0000000..97befdd
Binary files /dev/null and b/AXNode/Assets/Cursor/Draw.cur differ
diff --git a/AXNode/Assets/Cursor/Insert.cur b/AXNode/Assets/Cursor/Insert.cur
new file mode 100644
index 0000000..ab10f76
Binary files /dev/null and b/AXNode/Assets/Cursor/Insert.cur differ
diff --git a/AXNode/Assets/Cursor/Move.cur b/AXNode/Assets/Cursor/Move.cur
new file mode 100644
index 0000000..649496b
Binary files /dev/null and b/AXNode/Assets/Cursor/Move.cur differ
diff --git a/AXNode/Assets/Cursor/MoveBottom.cur b/AXNode/Assets/Cursor/MoveBottom.cur
new file mode 100644
index 0000000..54329b2
Binary files /dev/null and b/AXNode/Assets/Cursor/MoveBottom.cur differ
diff --git a/AXNode/Assets/Cursor/MoveSelected.cur b/AXNode/Assets/Cursor/MoveSelected.cur
new file mode 100644
index 0000000..2d1875d
Binary files /dev/null and b/AXNode/Assets/Cursor/MoveSelected.cur differ
diff --git a/AXNode/Assets/Cursor/MoveTop.cur b/AXNode/Assets/Cursor/MoveTop.cur
new file mode 100644
index 0000000..279c4a2
Binary files /dev/null and b/AXNode/Assets/Cursor/MoveTop.cur differ
diff --git a/AXNode/Assets/Cursor/MoveX.cur b/AXNode/Assets/Cursor/MoveX.cur
new file mode 100644
index 0000000..16b03fc
Binary files /dev/null and b/AXNode/Assets/Cursor/MoveX.cur differ
diff --git a/AXNode/Assets/Cursor/MoveY.cur b/AXNode/Assets/Cursor/MoveY.cur
new file mode 100644
index 0000000..51e228f
Binary files /dev/null and b/AXNode/Assets/Cursor/MoveY.cur differ
diff --git a/AXNode/Assets/Cursor/OnOff.cur b/AXNode/Assets/Cursor/OnOff.cur
new file mode 100644
index 0000000..a9d5795
Binary files /dev/null and b/AXNode/Assets/Cursor/OnOff.cur differ
diff --git a/AXNode/Assets/Cursor/ResizeDownUp.cur b/AXNode/Assets/Cursor/ResizeDownUp.cur
new file mode 100644
index 0000000..203232d
Binary files /dev/null and b/AXNode/Assets/Cursor/ResizeDownUp.cur differ
diff --git a/AXNode/Assets/Cursor/ResizeUpDown.cur b/AXNode/Assets/Cursor/ResizeUpDown.cur
new file mode 100644
index 0000000..12f1068
Binary files /dev/null and b/AXNode/Assets/Cursor/ResizeUpDown.cur differ
diff --git a/AXNode/Assets/Cursor/Select.cur b/AXNode/Assets/Cursor/Select.cur
new file mode 100644
index 0000000..83be813
Binary files /dev/null and b/AXNode/Assets/Cursor/Select.cur differ
diff --git a/AXNode/Assets/Font/simsun.ttc b/AXNode/Assets/Font/simsun.ttc
new file mode 100644
index 0000000..40e9693
Binary files /dev/null and b/AXNode/Assets/Font/simsun.ttc differ
diff --git a/AXNode/Assets/Icon15/Add.png b/AXNode/Assets/Icon15/Add.png
new file mode 100644
index 0000000..49dcd3c
Binary files /dev/null and b/AXNode/Assets/Icon15/Add.png differ
diff --git a/AXNode/Assets/Icon15/Browse.png b/AXNode/Assets/Icon15/Browse.png
new file mode 100644
index 0000000..d76605c
Binary files /dev/null and b/AXNode/Assets/Icon15/Browse.png differ
diff --git a/AXNode/Assets/Icon15/Clear.png b/AXNode/Assets/Icon15/Clear.png
new file mode 100644
index 0000000..a533135
Binary files /dev/null and b/AXNode/Assets/Icon15/Clear.png differ
diff --git a/AXNode/Assets/Icon15/ControlBoard.png b/AXNode/Assets/Icon15/ControlBoard.png
new file mode 100644
index 0000000..678ff49
Binary files /dev/null and b/AXNode/Assets/Icon15/ControlBoard.png differ
diff --git a/AXNode/Assets/Icon15/Copy.png b/AXNode/Assets/Icon15/Copy.png
new file mode 100644
index 0000000..9cde699
Binary files /dev/null and b/AXNode/Assets/Icon15/Copy.png differ
diff --git a/AXNode/Assets/Icon15/Delete.png b/AXNode/Assets/Icon15/Delete.png
new file mode 100644
index 0000000..167f376
Binary files /dev/null and b/AXNode/Assets/Icon15/Delete.png differ
diff --git a/AXNode/Assets/Icon15/EmptyFolder.png b/AXNode/Assets/Icon15/EmptyFolder.png
new file mode 100644
index 0000000..6bf4013
Binary files /dev/null and b/AXNode/Assets/Icon15/EmptyFolder.png differ
diff --git a/AXNode/Assets/Icon15/File.png b/AXNode/Assets/Icon15/File.png
new file mode 100644
index 0000000..dbfce55
Binary files /dev/null and b/AXNode/Assets/Icon15/File.png differ
diff --git a/AXNode/Assets/Icon15/Folder.png b/AXNode/Assets/Icon15/Folder.png
new file mode 100644
index 0000000..f9f65b8
Binary files /dev/null and b/AXNode/Assets/Icon15/Folder.png differ
diff --git a/AXNode/Assets/Icon15/Lib.png b/AXNode/Assets/Icon15/Lib.png
new file mode 100644
index 0000000..8bdc82e
Binary files /dev/null and b/AXNode/Assets/Icon15/Lib.png differ
diff --git a/AXNode/Assets/Icon15/Node.png b/AXNode/Assets/Icon15/Node.png
new file mode 100644
index 0000000..18ff10b
Binary files /dev/null and b/AXNode/Assets/Icon15/Node.png differ
diff --git a/AXNode/Assets/Icon15/NodePreset.png b/AXNode/Assets/Icon15/NodePreset.png
new file mode 100644
index 0000000..f75d206
Binary files /dev/null and b/AXNode/Assets/Icon15/NodePreset.png differ
diff --git a/AXNode/Assets/Icon15/Remove.png b/AXNode/Assets/Icon15/Remove.png
new file mode 100644
index 0000000..635853f
Binary files /dev/null and b/AXNode/Assets/Icon15/Remove.png differ
diff --git a/AXNode/Assets/Icon15/Rename.png b/AXNode/Assets/Icon15/Rename.png
new file mode 100644
index 0000000..f6dc127
Binary files /dev/null and b/AXNode/Assets/Icon15/Rename.png differ
diff --git a/AXNode/Assets/Icon16/AlignCenter.png b/AXNode/Assets/Icon16/AlignCenter.png
new file mode 100644
index 0000000..5bc8b4b
Binary files /dev/null and b/AXNode/Assets/Icon16/AlignCenter.png differ
diff --git a/AXNode/Assets/Icon16/AlignLeft.png b/AXNode/Assets/Icon16/AlignLeft.png
new file mode 100644
index 0000000..d691806
Binary files /dev/null and b/AXNode/Assets/Icon16/AlignLeft.png differ
diff --git a/AXNode/Assets/Icon16/AlignRight.png b/AXNode/Assets/Icon16/AlignRight.png
new file mode 100644
index 0000000..b58516c
Binary files /dev/null and b/AXNode/Assets/Icon16/AlignRight.png differ
diff --git a/AXNode/Assets/Icon16/ClearConsole.png b/AXNode/Assets/Icon16/ClearConsole.png
new file mode 100644
index 0000000..64eca20
Binary files /dev/null and b/AXNode/Assets/Icon16/ClearConsole.png differ
diff --git a/AXNode/Assets/Icon16/CloseFile.png b/AXNode/Assets/Icon16/CloseFile.png
new file mode 100644
index 0000000..a4be1fa
Binary files /dev/null and b/AXNode/Assets/Icon16/CloseFile.png differ
diff --git a/AXNode/Assets/Icon16/Console.png b/AXNode/Assets/Icon16/Console.png
new file mode 100644
index 0000000..462686f
Binary files /dev/null and b/AXNode/Assets/Icon16/Console.png differ
diff --git a/AXNode/Assets/Icon16/ExpandAll.png b/AXNode/Assets/Icon16/ExpandAll.png
new file mode 100644
index 0000000..464d67b
Binary files /dev/null and b/AXNode/Assets/Icon16/ExpandAll.png differ
diff --git a/AXNode/Assets/Icon16/FurlAll.png b/AXNode/Assets/Icon16/FurlAll.png
new file mode 100644
index 0000000..b4e2e9c
Binary files /dev/null and b/AXNode/Assets/Icon16/FurlAll.png differ
diff --git a/AXNode/Assets/Icon16/NewFile.png b/AXNode/Assets/Icon16/NewFile.png
new file mode 100644
index 0000000..79d52bf
Binary files /dev/null and b/AXNode/Assets/Icon16/NewFile.png differ
diff --git a/AXNode/Assets/Icon16/Node/CPU.png b/AXNode/Assets/Icon16/Node/CPU.png
new file mode 100644
index 0000000..263aa5c
Binary files /dev/null and b/AXNode/Assets/Icon16/Node/CPU.png differ
diff --git a/AXNode/Assets/Icon16/Node/Calculator.png b/AXNode/Assets/Icon16/Node/Calculator.png
new file mode 100644
index 0000000..44b3b7b
Binary files /dev/null and b/AXNode/Assets/Icon16/Node/Calculator.png differ
diff --git a/AXNode/Assets/Icon16/Node/Chip.png b/AXNode/Assets/Icon16/Node/Chip.png
new file mode 100644
index 0000000..3b02f40
Binary files /dev/null and b/AXNode/Assets/Icon16/Node/Chip.png differ
diff --git a/AXNode/Assets/Icon16/Node/Delay.png b/AXNode/Assets/Icon16/Node/Delay.png
new file mode 100644
index 0000000..2972b1f
Binary files /dev/null and b/AXNode/Assets/Icon16/Node/Delay.png differ
diff --git a/AXNode/Assets/Icon16/Node/Fan.png b/AXNode/Assets/Icon16/Node/Fan.png
new file mode 100644
index 0000000..aefad14
Binary files /dev/null and b/AXNode/Assets/Icon16/Node/Fan.png differ
diff --git a/AXNode/Assets/Icon16/Node/Flow.png b/AXNode/Assets/Icon16/Node/Flow.png
new file mode 100644
index 0000000..44382f0
Binary files /dev/null and b/AXNode/Assets/Icon16/Node/Flow.png differ
diff --git a/AXNode/Assets/Icon16/Node/FrameDriver.png b/AXNode/Assets/Icon16/Node/FrameDriver.png
new file mode 100644
index 0000000..9363e36
Binary files /dev/null and b/AXNode/Assets/Icon16/Node/FrameDriver.png differ
diff --git a/AXNode/Assets/Icon16/Node/Function.png b/AXNode/Assets/Icon16/Node/Function.png
new file mode 100644
index 0000000..baea615
Binary files /dev/null and b/AXNode/Assets/Icon16/Node/Function.png differ
diff --git a/AXNode/Assets/Icon16/Node/Key.png b/AXNode/Assets/Icon16/Node/Key.png
new file mode 100644
index 0000000..3ca162d
Binary files /dev/null and b/AXNode/Assets/Icon16/Node/Key.png differ
diff --git a/AXNode/Assets/Icon16/Node/Loop.png b/AXNode/Assets/Icon16/Node/Loop.png
new file mode 100644
index 0000000..32f0369
Binary files /dev/null and b/AXNode/Assets/Icon16/Node/Loop.png differ
diff --git a/AXNode/Assets/Icon16/Node/Message.png b/AXNode/Assets/Icon16/Node/Message.png
new file mode 100644
index 0000000..1be99ac
Binary files /dev/null and b/AXNode/Assets/Icon16/Node/Message.png differ
diff --git a/AXNode/Assets/Icon16/Node/Midi.png b/AXNode/Assets/Icon16/Node/Midi.png
new file mode 100644
index 0000000..902bf01
Binary files /dev/null and b/AXNode/Assets/Icon16/Node/Midi.png differ
diff --git a/AXNode/Assets/Icon16/Node/Net.png b/AXNode/Assets/Icon16/Node/Net.png
new file mode 100644
index 0000000..56b0448
Binary files /dev/null and b/AXNode/Assets/Icon16/Node/Net.png differ
diff --git a/AXNode/Assets/Icon16/Node/Node.png b/AXNode/Assets/Icon16/Node/Node.png
new file mode 100644
index 0000000..8147874
Binary files /dev/null and b/AXNode/Assets/Icon16/Node/Node.png differ
diff --git a/AXNode/Assets/Icon16/Node/Pause.png b/AXNode/Assets/Icon16/Node/Pause.png
new file mode 100644
index 0000000..09c26cc
Binary files /dev/null and b/AXNode/Assets/Icon16/Node/Pause.png differ
diff --git a/AXNode/Assets/Icon16/Node/Spectrum.png b/AXNode/Assets/Icon16/Node/Spectrum.png
new file mode 100644
index 0000000..4e8a274
Binary files /dev/null and b/AXNode/Assets/Icon16/Node/Spectrum.png differ
diff --git a/AXNode/Assets/Icon16/Node/Timer.png b/AXNode/Assets/Icon16/Node/Timer.png
new file mode 100644
index 0000000..c0e5ccc
Binary files /dev/null and b/AXNode/Assets/Icon16/Node/Timer.png differ
diff --git a/AXNode/Assets/Icon16/Node/Variate.png b/AXNode/Assets/Icon16/Node/Variate.png
new file mode 100644
index 0000000..b088cf0
Binary files /dev/null and b/AXNode/Assets/Icon16/Node/Variate.png differ
diff --git a/AXNode/Assets/Icon16/Node/Volume.png b/AXNode/Assets/Icon16/Node/Volume.png
new file mode 100644
index 0000000..775c9af
Binary files /dev/null and b/AXNode/Assets/Icon16/Node/Volume.png differ
diff --git a/AXNode/Assets/Icon16/OpenFile.png b/AXNode/Assets/Icon16/OpenFile.png
new file mode 100644
index 0000000..6221129
Binary files /dev/null and b/AXNode/Assets/Icon16/OpenFile.png differ
diff --git a/AXNode/Assets/Icon16/Redo.png b/AXNode/Assets/Icon16/Redo.png
new file mode 100644
index 0000000..2c68f91
Binary files /dev/null and b/AXNode/Assets/Icon16/Redo.png differ
diff --git a/AXNode/Assets/Icon16/Rename.png b/AXNode/Assets/Icon16/Rename.png
new file mode 100644
index 0000000..ecdb93c
Binary files /dev/null and b/AXNode/Assets/Icon16/Rename.png differ
diff --git a/AXNode/Assets/Icon16/Revert.png b/AXNode/Assets/Icon16/Revert.png
new file mode 100644
index 0000000..8c0e823
Binary files /dev/null and b/AXNode/Assets/Icon16/Revert.png differ
diff --git a/AXNode/Assets/Icon16/Save.png b/AXNode/Assets/Icon16/Save.png
new file mode 100644
index 0000000..be4c200
Binary files /dev/null and b/AXNode/Assets/Icon16/Save.png differ
diff --git a/AXNode/Assets/Icon16/SaveAll.png b/AXNode/Assets/Icon16/SaveAll.png
new file mode 100644
index 0000000..6d8f19e
Binary files /dev/null and b/AXNode/Assets/Icon16/SaveAll.png differ
diff --git a/AXNode/Assets/Icon16/SaveAs.png b/AXNode/Assets/Icon16/SaveAs.png
new file mode 100644
index 0000000..758d08a
Binary files /dev/null and b/AXNode/Assets/Icon16/SaveAs.png differ
diff --git a/AXNode/Assets/Icon16/Undo.png b/AXNode/Assets/Icon16/Undo.png
new file mode 100644
index 0000000..f924deb
Binary files /dev/null and b/AXNode/Assets/Icon16/Undo.png differ
diff --git a/AXNode/Assets/PinIcons.axaml b/AXNode/Assets/PinIcons.axaml
new file mode 100644
index 0000000..68d7bb2
--- /dev/null
+++ b/AXNode/Assets/PinIcons.axaml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/AXNode/CoreEditer.axaml b/AXNode/CoreEditer.axaml
new file mode 100644
index 0000000..459832e
--- /dev/null
+++ b/AXNode/CoreEditer.axaml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AXNode/CoreEditer.axaml.cs b/AXNode/CoreEditer.axaml.cs
new file mode 100644
index 0000000..22284de
--- /dev/null
+++ b/AXNode/CoreEditer.axaml.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Collections.Generic;
+using System.Windows;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using XLib.Node;
+using AXNode.SubSystem.NodeEditSystem.Define;
+using AXNode.SubSystem.ProjectSystem;
+using XLib.Base;
+using XLib.Base.VirtualDisk;
+
+namespace AXNode
+{
+ public partial class CoreEditer : UserControl
+ {
+ #region 属性
+
+ /// 节点列表
+ public List NodeList => Panel_NodeEditer.NodeList;
+
+ #endregion
+
+ #region 构造方法
+
+ public CoreEditer()
+ {
+ InitializeComponent();
+ Loaded += CoreEditer_Loaded;
+ }
+
+ #endregion
+
+ #region 公开方法
+
+ ///
+ /// 重置编辑器
+ ///
+ public void ResetEditer() => Panel_NodeEditer.Reset();
+
+ ///
+ /// 加载节点
+ ///
+ public void LoadNode(NodeBase node) => Panel_NodeEditer.LoadNode(node);
+
+ ///
+ /// 查找引脚
+ ///
+ public PinBase? FindPin(PinPath path) => Panel_NodeEditer.FindPin(path);
+
+ #endregion
+
+ #region 控件事件
+
+ private void CoreEditer_Loaded(object sender, RoutedEventArgs e)
+ {
+ Init();
+ ProjectManager.Instance.NewProject();
+ ProjectManager.Instance.Saved = true;
+ }
+
+ #endregion
+
+ #region 私有方法
+
+ ///
+ /// 初始化核心编辑器
+ ///
+ private void Init()
+ {
+ // 初始化编辑器面板
+ Panel_NodeEditer.Init();
+ // 初始化节点库面板
+ Panel_NodeLib.Init();
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/AXNode/MainWindow.axaml b/AXNode/MainWindow.axaml
new file mode 100644
index 0000000..789df00
--- /dev/null
+++ b/AXNode/MainWindow.axaml
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AXNode/MainWindow.axaml.cs b/AXNode/MainWindow.axaml.cs
new file mode 100644
index 0000000..865e7d1
--- /dev/null
+++ b/AXNode/MainWindow.axaml.cs
@@ -0,0 +1,259 @@
+using System;
+using System.Threading.Tasks;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Media;
+using Avalonia.Styling;
+using AXNode.AppTool;
+using AXNode.Sample.Layer;
+using AXNode.SubSystem.CacheSystem;
+using AXNode.SubSystem.EventSystem;
+using AXNode.SubSystem.ProjectSystem;
+using AXNode.SubSystem.ResourceSystem;
+using AXNode.SubSystem.WindowSystem;
+using XLib.Avalonia.WindowDefine;
+using XLib.AvaloniaControl;
+using XLib.Base;
+
+namespace AXNode;
+
+public partial class MainWindow : Window
+{
+ #region 属性
+
+ /// 核心编辑器实例
+ public CoreEditer Editer
+ {
+ get
+ {
+ if (_coreEditer == null) throw new Exception("核心编辑器为空");
+ return _coreEditer;
+ }
+ }
+
+ #endregion
+
+ #region 构造方法
+
+ public MainWindow() => InitializeComponent();
+
+ #endregion
+
+ protected override void OnLoaded(RoutedEventArgs e)
+ {
+ base.OnLoaded(e);
+ Closing += OnClosing;
+ WindowLoaded();
+ AddHandler(KeyDownEvent, (sender, args) => { Keyboard.RegisterModifiers(args.KeyModifiers); },
+ RoutingStrategies.Tunnel);
+ AddHandler(PointerMovedEvent, (sender, args) =>
+ {
+ Mouse.RegisterPointerEventArgs(args);
+ Mouse.RegisterDirectlyOver(args);
+ }, RoutingStrategies.Tunnel);
+ }
+
+ private async void OnClosing(object? sender, WindowClosingEventArgs e)
+ {
+ e.Cancel = await ReadyClose() == true;
+ }
+
+
+ #region XMainWindow 方法
+
+ protected void WindowLoaded()
+ {
+ // 恢复窗口状态并监听窗口状态
+ RecoverWindowState();
+ ListenWindowState();
+
+ // 设置主窗口实例
+ WM.Main = this;
+
+ // 初始化工具栏
+ // InitToolBar();
+
+ // 加载核心编辑器
+ LoadCoreEditer();
+
+ // 监听系统事件
+ EM.Instance.Add(EventType.Project_Changed, UpdateTitle);
+ }
+
+ protected async Task ReadyClose()
+ {
+ // 项目未保存
+ if (!ProjectManager.Instance.Saved)
+ {
+ bool? result = await WM.ShowAsk("当前项目未保存,是否保存?");
+ // 保存
+ if (result == true)
+ {
+ bool saved = ProjectManager.Instance.SaveProject();
+ // 确定保存,但未执行,则取消操作
+ if (!saved) return false;
+ }
+ // 取消操作
+ else if (result == null) return false;
+ }
+
+ // 关闭项目
+ ProjectManager.Instance.CloseProject();
+
+ return true;
+ }
+
+ #endregion
+
+ #region 窗口事件
+
+ private void XMainWindow_PreviewKeyDown(object? sender, KeyEventArgs e)
+ {
+ EM.Instance.Invoke(EventType.KeyDown, e.Key.ToString());
+ }
+
+ private void XMainWindow_PreviewKeyUp(object? sender, KeyEventArgs e)
+ {
+ EM.Instance.Invoke(EventType.KeyUp, e.Key.ToString());
+ }
+
+ #endregion
+
+ #region 私有方法
+
+ ///
+ /// 恢复窗口状态
+ ///
+ private void RecoverWindowState()
+ {
+ WindowState = CacheManager.Instance.Cache.MainWindow.State;
+ var screenFromVisual = this.Screens.ScreenFromVisual(this);
+ // 居中窗口
+ Bounds = new Rect((screenFromVisual.WorkingArea.Width - Width) / 2,
+ (screenFromVisual.WorkingArea.Height - Height) / 2, CacheManager.Instance.Cache.MainWindow.Width,
+ CacheManager.Instance.Cache.MainWindow.Height);
+ }
+
+ ///
+ /// 监听窗口状态
+ ///
+ private void ListenWindowState()
+ {
+ // StateChanged += (s, e) =>
+ // {
+ // if (WindowState is WindowState.Normal or WindowState.Maximized)
+ // {
+ // CacheManager.Instance.Cache.MainWindow.State = WindowState;
+ // CacheManager.Instance.UpdateCache();
+ // }
+ // };
+ SizeChanged += (s, e) =>
+ {
+ if (WindowState == WindowState.Maximized) return;
+ CacheManager.Instance.Cache.MainWindow.Width = (int)Width;
+ CacheManager.Instance.Cache.MainWindow.Height = (int)Height;
+ CacheManager.Instance.UpdateCache();
+ };
+ }
+
+
+ ///
+ /// 工具栏.单击工具
+ ///
+ private void ToolBar_ToolClick(string name)
+ {
+ switch (name)
+ {
+ // 新建项目
+ case "NewProject":
+ ProjectManager.Instance.NewProject();
+ UpdateTitle();
+ break;
+ // 打开项目
+ case "OpenProject":
+ ProjectManager.Instance.OpenProject();
+ UpdateTitle();
+ break;
+ // 保存项目
+ case "SaveProject":
+ ProjectManager.Instance.SaveProject();
+ UpdateTitle();
+ break;
+ // 另存为项目
+ case "SaveAs":
+ ProjectManager.Instance.SaveAsProject();
+ UpdateTitle();
+ break;
+ // 撤销
+ case "Undo":
+ break;
+ // 重做
+ case "Redo":
+ break;
+ // 控制台
+ case "Console":
+ if (_consoleOpened)
+ {
+ OSTool.CloseConsole();
+ _consoleOpened = false;
+ }
+ else
+ {
+ OSTool.OpenConsole();
+ _consoleOpened = true;
+ }
+
+ break;
+ // 清空控制台
+ case "ClearConsole":
+ Console.Clear();
+ break;
+ }
+ }
+
+ ///
+ /// 加载核心编辑器
+ ///
+ private void LoadCoreEditer()
+ {
+ _coreEditer = new CoreEditer { Margin = new Thickness(0, 2, 0, 0) };
+ MainGrid.Children.Add(_coreEditer);
+ }
+
+ ///
+ /// 更新标题
+ ///
+ private void UpdateTitle()
+ {
+ if (ProjectManager.Instance.CurrentProject != null)
+ {
+ Title = ProjectManager.Instance.CurrentProject.ProjectName;
+ if (!ProjectManager.Instance.Saved) Title += "*";
+ Title += " - " + AppDelegate.AppTitle;
+ }
+ else Title = AppDelegate.AppTitle;
+ }
+
+ #endregion
+
+ #region 字段
+
+ /// 工具栏
+ // private ToolBar? _toolBar = null;
+
+ /// 核心编辑器
+ private CoreEditer? _coreEditer = null;
+
+ /// 控制台已打开
+ private bool _consoleOpened = false;
+
+ #endregion
+
+
+ private void MenuItem_OnClick(object? sender, RoutedEventArgs e)
+ {
+ ToolBar_ToolClick((sender as MenuItem).Tag.ToString());
+ }
+}
\ No newline at end of file
diff --git a/AXNode/Program.cs b/AXNode/Program.cs
new file mode 100644
index 0000000..fa46a1b
--- /dev/null
+++ b/AXNode/Program.cs
@@ -0,0 +1,21 @@
+using Avalonia;
+using System;
+
+namespace AXNode;
+
+class Program
+{
+ // Initialization code. Don't use any Avalonia, third-party APIs or any
+ // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
+ // yet and stuff might break.
+ [STAThread]
+ public static void Main(string[] args) => BuildAvaloniaApp()
+ .StartWithClassicDesktopLifetime(args);
+
+ // Avalonia configuration, don't remove; also used by visual designer.
+ public static AppBuilder BuildAvaloniaApp()
+ => AppBuilder.Configure()
+ .UsePlatformDetect()
+ .WithInterFont()
+ .LogToTrace();
+}
\ No newline at end of file
diff --git a/AXNode/Sample/Layer/LoadingLayer.cs b/AXNode/Sample/Layer/LoadingLayer.cs
new file mode 100644
index 0000000..7a1ee64
--- /dev/null
+++ b/AXNode/Sample/Layer/LoadingLayer.cs
@@ -0,0 +1,104 @@
+using Avalonia;
+using Avalonia.Media;
+using Avalonia.Threading;
+using XLib.Animate;
+using XLib.Avalonia.Drawing;
+using XLib.Math.Easing;
+
+namespace AXNode.Sample.Layer
+{
+ ///
+ /// 加载图层。播放加载动画,测试动画用
+ ///
+ public class LoadingLayer : SingleBoard, IMotion
+ {
+ public void Start()
+ {
+ // 创建并添加动画队列
+ _group.Add(this.CreateAnimation("offset", 0, 200, 4300, loop: true));
+ _group.Add(CreatQueue("x1", 0, 0, 200, 400, 800));
+ _group.Add(CreatQueue("x2", 200, 0 - 8, 200 - 8, 400 - 8, 600));
+ _group.Add(CreatQueue("x3", 400, 0 - 16, 200 - 16, 400 - 16, 400));
+ _group.Add(CreatQueue("x4", 600, 0 - 24, 200 - 24, 400 - 24, 200));
+ _group.Add(CreatQueue("x5", 800, 0 - 32, 200 - 32, 400 - 32, 0));
+ // 添加动画
+ AnimationEngine.Instance.AddAnimation(_group);
+ }
+
+ private AnimationQueue CreatQueue(string property, double leftDelay, double x1, double x2, double x3,
+ double rightDelay)
+ {
+ AnimationQueue queue = new AnimationQueue { Loop = true };
+ queue.AnimationList.Add(new AnimationDelay { Duration = leftDelay });
+ queue.AnimationList.Add(this.CreateAnimation(property, x1, x2, 1000, EasingType.QuinticEase,
+ EasingMode.EaseOut));
+ queue.AnimationList.Add(new AnimationDelay { Duration = 500 });
+ queue.AnimationList.Add(this.CreateAnimation(property, x2, x3, 1000, EasingType.QuinticEase,
+ EasingMode.EaseIn));
+ queue.AnimationList.Add(new AnimationDelay { Duration = 1000 });
+ queue.AnimationList.Add(new AnimationDelay { Duration = rightDelay });
+ queue.Init();
+ return queue;
+ }
+
+ private void Group_Finished(IAnimation sender)
+ {
+ Dispatcher.UIThread.Invoke(Clear);
+ }
+
+ public void Stop()
+ {
+ _group.Stop();
+ }
+
+ // public override void Init() => _brush.Freeze();
+ public override void Render(DrawingContext context)
+ {
+ DrawVertex(context, new Point(_x1 + _offset, 0), 4, _brush, null);
+ DrawVertex(context, new Point(_x2 + _offset, 0), 4, _brush, null);
+ DrawVertex(context, new Point(_x3 + _offset, 0), 4, _brush, null);
+ DrawVertex(context, new Point(_x4 + _offset, 0), 4, _brush, null);
+ DrawVertex(context, new Point(_x5 + _offset, 0), 4, _brush, null);
+ }
+
+ public double GetMotionProperty(string propertyName) => 0;
+
+ public void SetMotionProperty(string propertyName, double value)
+ {
+ switch (propertyName)
+ {
+ case "offset":
+ _offset = value;
+ break;
+ case "x1":
+ _x1 = value;
+ break;
+ case "x2":
+ _x2 = value;
+ break;
+ case "x3":
+ _x3 = value;
+ break;
+ case "x4":
+ _x4 = value;
+ break;
+ case "x5":
+ _x5 = value;
+ break;
+ }
+
+ Dispatcher.UIThread.Invoke(Update);
+ }
+
+ private double _offset = 0;
+ private double _x1 = 0;
+ private double _x2 = 0;
+ private double _x3 = 0;
+ private double _x4 = 0;
+ private double _x5 = 0;
+
+ private AnimationGroup _group = new AnimationGroup();
+
+ private readonly IBrush _brush = new SolidColorBrush(Colors.Black);
+ }
+}
\ No newline at end of file
diff --git a/AXNode/Sample/Layer/TargetPointLayer.cs b/AXNode/Sample/Layer/TargetPointLayer.cs
new file mode 100644
index 0000000..47137cb
--- /dev/null
+++ b/AXNode/Sample/Layer/TargetPointLayer.cs
@@ -0,0 +1,59 @@
+using System;
+using Avalonia;
+using Avalonia.Media;
+using Avalonia.Threading;
+using XLib.Animate;
+using XLib.Avalonia.Drawing;
+using XLib.Math.Easing;
+
+namespace XLib.Sample.Layer
+{
+ ///
+ /// 目标点图层。测试动画用
+ ///
+ public class TargetPointLayer : SingleBoard, IMotion
+ {
+ public Point TargetPoint { get; set; } = new Point();
+
+ public override void Render(DrawingContext context)
+ {
+ Opacity = _opacity;
+ context.DrawEllipse(null, _pen, TargetPoint, _size / 2, _size / 2);
+ }
+
+ public double GetMotionProperty(string propertyName) => 0;
+
+ public void SetMotionProperty(string propertyName, double value)
+ {
+ switch (propertyName)
+ {
+ case "Size":
+ _size = value;
+ Console.WriteLine(_size);
+ break;
+ case "Alpha":
+ _opacity = value;
+ break;
+ }
+
+ Dispatcher.UIThread.Invoke(Update);
+ }
+
+ public void SetTargetPoint(Point point)
+ {
+ TargetPoint = point;
+ // 创建动画组
+ AnimationGroup group = new AnimationGroup();
+ group.Add(
+ this.CreateAnimation("Size", 0, 400, 2800, EasingType.QuinticEase, EasingMode.EaseOut, loop: true));
+ group.Add(this.CreateAnimation("Alpha", 1, 0, 2800, loop: true));
+ // 添加动画组
+ AnimationEngine.Instance.AddAnimation(group);
+ }
+
+ private double _size = 0;
+ private double _opacity = 1;
+
+ private readonly Pen _pen = new Pen(Brushes.Black, 1);
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/ArchiveSystem/ArchiveManager.cs b/AXNode/SubSystem/ArchiveSystem/ArchiveManager.cs
new file mode 100644
index 0000000..dcca4c3
--- /dev/null
+++ b/AXNode/SubSystem/ArchiveSystem/ArchiveManager.cs
@@ -0,0 +1,148 @@
+using System;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using System.IO;
+using System.Text;
+using XLib.Base.ArchiveFrame;
+using AXNode.SubSystem.ArchiveSystem.Define.Data_1_0;
+using AXNode.SubSystem.ArchiveSystem.Loader;
+using AXNode.SubSystem.WindowSystem;
+
+namespace AXNode.SubSystem.ArchiveSystem
+{
+ ///
+ /// 存档管理器
+ ///
+ public class ArchiveManager
+ {
+ #region 单例
+
+ private ArchiveManager()
+ {
+ }
+
+ public static ArchiveManager Instance { get; } = new ArchiveManager();
+
+ #endregion
+
+ #region 属性
+
+ /// 当前版本
+ public string CurrentVersion { get; private set; } = "1.0";
+
+ #endregion
+
+ #region 公开方法
+
+ ///
+ /// 生成存档
+ ///
+ public ArchiveFile GenerateArchive()
+ {
+ return new ArchiveFile
+ {
+ Version = CurrentVersion,
+ Data = Extracter.Extract(),
+ };
+ }
+
+ ///
+ /// 读取存档文件
+ ///
+ public ArchiveFile? ReadArchiveFile(string filePath)
+ {
+ string jsonData = File.ReadAllText(filePath, Encoding.UTF8);
+ return JsonConvert.DeserializeObject(jsonData);
+ }
+
+ ///
+ /// 加载存档
+ ///
+ public bool LoadArchive(ArchiveFile file, string path, JObject originData)
+ {
+ // 检查存档版本
+ if (!CheckVersion(file))
+ {
+ WM.ShowError($"读取存档“{path}”失败:无效的版本");
+ return false;
+ }
+
+ // 比较版本,过新则不加载
+ int result = CompareVersion(file.Version);
+ if (result < 0)
+ {
+ WM.ShowTip("存档版本过新,请升级软件后重试");
+ return false;
+ }
+
+ // 导入存档数据
+ if (!ImportArchiveData(file, originData, path))
+ {
+ WM.ShowError($"存档“{path}”加载失败");
+ return false;
+ }
+
+ return true;
+ }
+
+ #endregion
+
+ #region 私有方法
+
+ ///
+ /// 检查版本
+ ///
+ private bool CheckVersion(ArchiveFile file)
+ {
+ if (string.IsNullOrEmpty(file.Version) || file.Version == "???") return false;
+ try
+ {
+ ArchiveVersion version = new ArchiveVersion(file.Version);
+ }
+ catch (Exception)
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// 比较版本
+ ///
+ private int CompareVersion(string version)
+ {
+ ArchiveVersion file = new ArchiveVersion(version);
+ ArchiveVersion current = new ArchiveVersion(CurrentVersion);
+ return file.CompareTo(current);
+ }
+
+ ///
+ /// 导入存档数据
+ ///
+ private bool ImportArchiveData(ArchiveFile file, JObject originData, string archiveFilePath)
+ {
+ try
+ {
+ switch (file.Version)
+ {
+ case "1.0":
+ Data_1_0? data_1_0 = originData.ToObject();
+ if (data_1_0 == null) return false;
+ if (!Loader_1_0.Import(data_1_0, archiveFilePath)) return false;
+ break;
+ default:
+ return false;
+ }
+
+ return true;
+ }
+ catch (Exception)
+ {
+ return false;
+ }
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/ArchiveSystem/Define/Data_1_0/ConnectLineData.cs b/AXNode/SubSystem/ArchiveSystem/Define/Data_1_0/ConnectLineData.cs
new file mode 100644
index 0000000..9769eb9
--- /dev/null
+++ b/AXNode/SubSystem/ArchiveSystem/Define/Data_1_0/ConnectLineData.cs
@@ -0,0 +1,14 @@
+namespace AXNode.SubSystem.ArchiveSystem.Define.Data_1_0
+{
+ ///
+ /// 连接线数据
+ ///
+ public class ConnectLineData
+ {
+ public string Start { get; set; } = "";
+
+ public string End { get; set; } = "";
+
+ public override string ToString() => $"{Start}-{End}";
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/ArchiveSystem/Define/Data_1_0/Data_1_0.cs b/AXNode/SubSystem/ArchiveSystem/Define/Data_1_0/Data_1_0.cs
new file mode 100644
index 0000000..5260b61
--- /dev/null
+++ b/AXNode/SubSystem/ArchiveSystem/Define/Data_1_0/Data_1_0.cs
@@ -0,0 +1,16 @@
+using System.Collections.Generic;
+
+namespace AXNode.SubSystem.ArchiveSystem.Define.Data_1_0
+{
+ ///
+ /// 存档数据
+ ///
+ public class Data_1_0
+ {
+ /// 节点列表
+ public List NodeList { get; set; } = new List();
+
+ /// 连接线列表
+ public List ConnectLineList { get; set; } = new List();
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/ArchiveSystem/Define/Data_1_0/NodeBaseData.cs b/AXNode/SubSystem/ArchiveSystem/Define/Data_1_0/NodeBaseData.cs
new file mode 100644
index 0000000..6f93089
--- /dev/null
+++ b/AXNode/SubSystem/ArchiveSystem/Define/Data_1_0/NodeBaseData.cs
@@ -0,0 +1,36 @@
+namespace AXNode.SubSystem.ArchiveSystem.Define.Data_1_0
+{
+ public class NodeBaseData
+ {
+ public NodeBaseData()
+ {
+ }
+
+ public NodeBaseData(string data)
+ {
+ string[] array = data.Split('/');
+ NodeLibName = array[0];
+ TypeString = array[1];
+ Version = array[2];
+ ID = int.Parse(array[3]);
+ Point = array[4];
+ }
+
+ /// 节点库名称
+ public string NodeLibName { get; set; } = "Inner";
+
+ /// 类型字符串
+ public string TypeString { get; set; } = "";
+
+ /// 节点版本
+ public string Version { get; set; } = "1.0";
+
+ /// 编号
+ public int ID { get; set; } = -1;
+
+ /// 坐标
+ public string Point { get; set; } = "0,0";
+
+ public override string ToString() => $"{NodeLibName}/{TypeString}/{Version}/{ID}/{Point}";
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/ArchiveSystem/Define/Data_1_0/NodeData.cs b/AXNode/SubSystem/ArchiveSystem/Define/Data_1_0/NodeData.cs
new file mode 100644
index 0000000..21a6404
--- /dev/null
+++ b/AXNode/SubSystem/ArchiveSystem/Define/Data_1_0/NodeData.cs
@@ -0,0 +1,19 @@
+using System.Collections.Generic;
+
+namespace AXNode.SubSystem.ArchiveSystem.Define.Data_1_0
+{
+ ///
+ /// 节点数据
+ ///
+ public class NodeData
+ {
+ /// 基本数据
+ public string BaseData { get; set; } = "";
+
+ /// 参数表
+ public Dictionary ParaDict { get; set; } = new Dictionary();
+
+ /// 属性表
+ public Dictionary PropertyDict { get; set; } = new Dictionary();
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/ArchiveSystem/Extracter.cs b/AXNode/SubSystem/ArchiveSystem/Extracter.cs
new file mode 100644
index 0000000..864c69b
--- /dev/null
+++ b/AXNode/SubSystem/ArchiveSystem/Extracter.cs
@@ -0,0 +1,93 @@
+using System.Collections.Generic;
+using Newtonsoft.Json;
+using XLib.Node;
+using AXNode.AppTool;
+using AXNode.SubSystem.ArchiveSystem.Define.Data_1_0;
+using AXNode.SubSystem.NodeEditSystem.Define;
+using AXNode.SubSystem.WindowSystem;
+
+namespace AXNode.SubSystem.ArchiveSystem;
+
+public class Extracter
+{
+ ///
+ /// 提取存档数据
+ ///
+ public static Data_1_0 Extract()
+ {
+ Data_1_0 result = new Data_1_0();
+
+ FillNodeList(result);
+ FillConnectLineList(result);
+
+ return result;
+ }
+
+ ///
+ /// 填充节点列表
+ ///
+ private static void FillNodeList(Data_1_0 data)
+ {
+ // 遍历节点
+ foreach (var node in WM.Main.Editer.NodeList)
+ {
+ // 基本数据
+ NodeBaseData baseData = new NodeBaseData
+ {
+ NodeLibName = node.NodeLibName,
+ TypeString = node.GetTypeString(),
+ Version = node.Version,
+ ID = node.ID,
+ Point = node.Point.ToString(),
+ };
+ // 参数与属性
+ NodeData nodeData = new NodeData
+ {
+ BaseData = baseData.ToString(),
+ ParaDict = node.GetParaDict(),
+ PropertyDict = node.GetPropertyDict(),
+ };
+ // 添加数据
+ data.NodeList.Add(JsonConvert.SerializeObject(nodeData));
+ }
+ }
+
+ ///
+ /// 填充连接线列表
+ ///
+ private static void FillConnectLineList(Data_1_0 data)
+ {
+ // 连接信息
+ Dictionary> connectInfo = new Dictionary>();
+ // 遍历节点,填充连接信息
+ foreach (var node in WM.Main.Editer.NodeList)
+ {
+ // 遍历全部引脚
+ foreach (var pin in node.GetAllPin())
+ {
+ // 忽略输入引脚与空输出引脚
+ if (pin.Flow == PinFlow.Input || pin.TargetList.Count == 0) continue;
+ // 添加连接源
+ if (!connectInfo.ContainsKey(pin)) connectInfo.Add(pin, new HashSet());
+ // 添加连接目标
+ foreach (var target in pin.TargetList) connectInfo[pin].Add(target);
+ }
+ }
+
+ // 遍历连接信息,填充连接线数据
+ foreach (var pair in connectInfo)
+ {
+ PinPath startPath = pair.Key.GetPinPath();
+ foreach (var targetPin in pair.Value)
+ {
+ PinPath endPath = targetPin.GetPinPath();
+ ConnectLineData lineData = new ConnectLineData
+ {
+ Start = startPath.ToString(),
+ End = endPath.ToString(),
+ };
+ data.ConnectLineList.Add(lineData.ToString());
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/ArchiveSystem/Loader/Loader_1_0.cs b/AXNode/SubSystem/ArchiveSystem/Loader/Loader_1_0.cs
new file mode 100644
index 0000000..d472742
--- /dev/null
+++ b/AXNode/SubSystem/ArchiveSystem/Loader/Loader_1_0.cs
@@ -0,0 +1,87 @@
+using System;
+using Newtonsoft.Json;
+using XLib.Node;
+using AXNode.SubSystem.ArchiveSystem.Define.Data_1_0;
+using AXNode.SubSystem.NodeEditSystem.Define;
+using AXNode.SubSystem.NodeLibSystem;
+using AXNode.SubSystem.WindowSystem;
+
+namespace AXNode.SubSystem.ArchiveSystem.Loader
+{
+ public class Loader_1_0
+ {
+ public static bool Import(Data_1_0 data, string archiveFilePath)
+ {
+ try
+ {
+ LoadNodeList(data);
+ LoadConnectLineList(data);
+ }
+ catch (Exception ex)
+ {
+#if DEBUG
+ throw ex;
+#endif
+ WM.ShowError("加载存档失败:" + ex.Message);
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// 加载节点列表
+ ///
+ private static void LoadNodeList(Data_1_0 data)
+ {
+ foreach (var nodeString in data.NodeList)
+ {
+ // 解析节点数据
+ NodeData? nodeData = JsonConvert.DeserializeObject(nodeString);
+ if (nodeData == null) continue;
+ // 解析节点基本数据
+ NodeBaseData? nodeBaseData = new NodeBaseData(nodeData.BaseData);
+ if (nodeBaseData == null) continue;
+ // 创建节点实例
+ NodeBase? node;
+ if (nodeBaseData.NodeLibName == "Inner")
+ node = NodeLibManager.Instance.CreateNode(nodeBaseData.TypeString);
+ else
+ node = NodeLibManager.Instance.CreateNode(nodeBaseData.NodeLibName, nodeBaseData.TypeString);
+ // 创建节点失败,则跳过
+ if (node == null) continue;
+ // 初始化节点
+ node.Init();
+ // 设置编号与坐标
+ node.ID = nodeBaseData.ID;
+ node.Point = new NodePoint(nodeBaseData.Point);
+ // 加载参数、属性
+ node.LoadParaDict(nodeBaseData.Version, nodeData.ParaDict);
+ node.LoadPropertyDict(nodeBaseData.Version, nodeData.PropertyDict);
+ // 加载节点至编辑器
+ WM.Main.Editer.LoadNode(node);
+ }
+ }
+
+ ///
+ /// 加载连接线列表
+ ///
+ private static void LoadConnectLineList(Data_1_0 data)
+ {
+ foreach (var lineString in data.ConnectLineList)
+ {
+ // 解析引脚路径
+ PinPath startPath = PinPath.ParsePinPath(lineString.Split('-')[0]);
+ PinPath endPath = PinPath.ParsePinPath(lineString.Split('-')[1]);
+ // 查找引脚
+ PinBase? startPin = WM.Main.Editer.FindPin(startPath);
+ if (startPin == null) continue;
+ PinBase? endPin = WM.Main.Editer.FindPin(endPath);
+ if (endPin == null) continue;
+ // 连接引脚
+ startPin.AddTarget(endPin);
+ endPin.AddSource(startPin);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/CacheSystem/CacheData.cs b/AXNode/SubSystem/CacheSystem/CacheData.cs
new file mode 100644
index 0000000..380650c
--- /dev/null
+++ b/AXNode/SubSystem/CacheSystem/CacheData.cs
@@ -0,0 +1,52 @@
+using System.Collections.Generic;
+using System.Windows;
+using Avalonia.Controls;
+using AXNode.SubSystem.CacheSystem;
+
+namespace AXNode.SubSystem.CacheSystem
+{
+ public class MainWindowCache
+ {
+ public WindowState State { get; set; } = WindowState.Normal;
+ public int Width { get; set; } = 1280;
+ public int Height { get; set; } = 720;
+ }
+
+ ///
+ /// 节点库缓存
+ ///
+ public class NodeLibCache
+ {
+ public List ExpandedFolderList { get; set; } = new List();
+
+ ///
+ /// 判断指定的文件夹是否处于展开状态
+ ///
+ public bool IsExpanded(string folderPath) => ExpandedFolderList.Contains(folderPath);
+
+ ///
+ /// 折叠指定文件夹
+ ///
+ public void Fold(string folderPath)
+ {
+ if (ExpandedFolderList.Contains(folderPath)) ExpandedFolderList.Remove(folderPath);
+ CacheManager.Instance.UpdateCache();
+ }
+
+ ///
+ /// 展开指定文件夹
+ ///
+ public void Expand(string folderPath)
+ {
+ if (!ExpandedFolderList.Contains(folderPath)) ExpandedFolderList.Add(folderPath);
+ CacheManager.Instance.UpdateCache();
+ }
+ }
+
+ public class CacheData
+ {
+ public MainWindowCache MainWindow { get; set; } = new MainWindowCache();
+
+ public NodeLibCache NodeLib { get; set; } = new NodeLibCache();
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/CacheSystem/CacheManager.cs b/AXNode/SubSystem/CacheSystem/CacheManager.cs
new file mode 100644
index 0000000..0040dc6
--- /dev/null
+++ b/AXNode/SubSystem/CacheSystem/CacheManager.cs
@@ -0,0 +1,92 @@
+using Newtonsoft.Json;
+using System.IO;
+using AXNode.SubSystem.CacheSystem;
+using XLib.Base;
+using AXNode.SubSystem.OptionSystem;
+
+namespace AXNode.SubSystem.CacheSystem
+{
+ ///
+ /// 缓存管理器
+ ///
+ public class CacheManager : IManager
+ {
+ #region 单例
+
+ private CacheManager()
+ {
+ }
+
+ public static CacheManager Instance { get; } = new CacheManager();
+
+ #endregion
+
+ #region 属性
+
+ public string Name { get; set; } = "缓存管理器";
+
+ /// 缓存数据
+ public CacheData Cache { get; set; } = new CacheData();
+
+ #endregion
+
+ #region “IManager”方法
+
+ public void Init()
+ {
+ // 不存在缓存,则新建缓存
+ if (!File.Exists($"{OptionManager.Instance.CachePath}\\{_cacheFileName}")) SaveCacheFile();
+ // 加载缓存文件
+ LoadCacheFile();
+ }
+
+ public void Reset()
+ {
+ }
+
+ public void Clear()
+ {
+ }
+
+ #endregion
+
+ #region 公开方法
+
+ ///
+ /// 更新缓存
+ ///
+ public void UpdateCache() => SaveCacheFile();
+
+ #endregion
+
+ #region 私有方法
+
+ ///
+ /// 保存缓存文件
+ ///
+ private void SaveCacheFile()
+ {
+ string filePath = $"{OptionManager.Instance.CachePath}\\{_cacheFileName}";
+ File.WriteAllText(filePath, JsonConvert.SerializeObject(Cache, Formatting.Indented));
+ }
+
+ ///
+ /// 加载缓存文件
+ ///
+ private void LoadCacheFile()
+ {
+ string jsonData = File.ReadAllText($"{OptionManager.Instance.CachePath}\\{_cacheFileName}");
+ CacheData? cache = JsonConvert.DeserializeObject(jsonData);
+ if (cache != null) Cache = cache;
+ }
+
+ #endregion
+
+ #region 字段
+
+ /// 缓存文件名
+ private readonly string _cacheFileName = "Cache.json";
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/ControlSystem/ControlEngine.cs b/AXNode/SubSystem/ControlSystem/ControlEngine.cs
new file mode 100644
index 0000000..c4b713b
--- /dev/null
+++ b/AXNode/SubSystem/ControlSystem/ControlEngine.cs
@@ -0,0 +1,67 @@
+using System.Collections.Generic;
+using XLib.Base.AppFrame;
+using XLib.Base.Drive;
+using AXNode.SubSystem.TimerSystem;
+
+namespace AXNode.SubSystem.ControlSystem
+{
+ public class ControlEngine : ServiceBase, ITimerHandler
+ {
+ #region 单例
+
+ private ControlEngine()
+ {
+ }
+
+ public static ControlEngine Instance { get; } = new ControlEngine();
+
+ #endregion
+
+ #region 生命周期
+
+ public override void Start() => TimeEngine.Instance.AddTimerHandler(this);
+
+ public override void Stop() => TimeEngine.Instance.RemoveHandler(this);
+
+ #endregion
+
+ #region 接口实现
+
+ public void Tick()
+ {
+ foreach (var frameDriver in _frameDriverList) frameDriver.Update();
+ }
+
+ #endregion
+
+ #region 公开方法
+
+ ///
+ /// 连接驱动器
+ ///
+ public void Connect(IFrameDriver frameDriver)
+ {
+ if (_frameDriverList.Contains(frameDriver)) return;
+ _frameDriverList = new List(_frameDriverList) { frameDriver };
+ }
+
+ ///
+ /// 断开驱动器
+ ///
+ public void Disconnect(IFrameDriver frameDriver)
+ {
+ List newList = new List(_frameDriverList);
+ newList.Remove(frameDriver);
+ _frameDriverList = newList;
+ }
+
+ #endregion
+
+ #region 字段
+
+ /// 帧驱动器列表
+ private List _frameDriverList = new List();
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/EventSystem/EM.cs b/AXNode/SubSystem/EventSystem/EM.cs
new file mode 100644
index 0000000..87cfbd5
--- /dev/null
+++ b/AXNode/SubSystem/EventSystem/EM.cs
@@ -0,0 +1,68 @@
+using System.Windows;
+using Avalonia.Threading;
+using XLib.Base;
+using AXNode.SubSystem.EventSystem;
+
+namespace AXNode.SubSystem.EventSystem
+{
+ public class EM : EM
+ {
+ #region 单例
+
+ private EM()
+ {
+ }
+
+ public static EM Instance { get; } = new EM();
+
+ #endregion
+
+ #region 引发事件
+
+ public void Invoke(EventType eventType)
+ {
+ InnerInvoke(eventType);
+ }
+
+ public void Invoke(EventType eventType, T1 value1)
+ {
+ InnerInvoke(eventType, value1);
+ }
+
+ public void Invoke(EventType eventType, T1 value1, T2 value2)
+ {
+ InnerInvoke(eventType, value1, value2);
+ }
+
+ public void Invoke(EventType eventType, T1 value1, T2 value2, T3 value3)
+ {
+ InnerInvoke(eventType, value1, value2, value3);
+ }
+
+ #endregion
+
+ #region 引发事件
+
+ public void BeginInvoke(EventType eventType)
+ {
+ Dispatcher.UIThread.Invoke(() => InnerInvoke(eventType));
+ }
+
+ public void BeginInvoke(EventType eventType, T1 value1)
+ {
+ Dispatcher.UIThread.Invoke(() => InnerInvoke(eventType, value1));
+ }
+
+ public void BeginInvoke(EventType eventType, T1 value1, T2 value2)
+ {
+ Dispatcher.UIThread.Invoke(() => InnerInvoke(eventType, value1, value2));
+ }
+
+ public void BeginInvoke(EventType eventType, T1 value1, T2 value2, T3 value3)
+ {
+ Dispatcher.UIThread.Invoke(() => InnerInvoke(eventType, value1, value2, value3));
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/EventSystem/EventType.cs b/AXNode/SubSystem/EventSystem/EventType.cs
new file mode 100644
index 0000000..15fbfbb
--- /dev/null
+++ b/AXNode/SubSystem/EventSystem/EventType.cs
@@ -0,0 +1,17 @@
+namespace AXNode.SubSystem.EventSystem
+{
+ public enum EventType
+ {
+ /// 按键按下
+ KeyDown,
+
+ /// 按键松开
+ KeyUp,
+
+ /// 项目已变更
+ Project_Changed,
+
+ /// 项目已加载
+ Project_Loaded,
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Control/ActionPinGroupView.axaml b/AXNode/SubSystem/NodeEditSystem/Control/ActionPinGroupView.axaml
new file mode 100644
index 0000000..17cdb41
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Control/ActionPinGroupView.axaml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Control/ActionPinGroupView.axaml.cs b/AXNode/SubSystem/NodeEditSystem/Control/ActionPinGroupView.axaml.cs
new file mode 100644
index 0000000..cea56f7
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Control/ActionPinGroupView.axaml.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Windows;
+using System.Windows.Input;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using XLib.Node;
+using AXNode.SubSystem.ResourceSystem;
+
+namespace AXNode.SubSystem.NodeEditSystem.Control
+{
+ public partial class ActionPinGroupView : PinGroupViewBase
+ {
+ public ActionPinGroupView() => InitializeComponent();
+
+ public ActionPinGroup? Instance { get; set; } = null;
+
+ public override void Init()
+ {
+ if (Instance == null) return;
+ Title_Pin.Text = Instance.ActionName;
+ Instance.ActionNameChanged = (name) => Title_Pin.Text = name;
+ Icon_Pin.Content = PinIconManager.Instance.ExecutePin_Null;
+
+ PinArea.PointerEntered += PinArea_MouseEnter;
+ PinArea.PointerExited += PinArea_MouseLeave;
+ }
+
+ public override Grid GetPinArea()
+ {
+ if (HoveredPin != null) return PinArea;
+ throw new Exception("无命中引脚");
+ }
+
+ private void PinArea_MouseEnter(object? sender, PointerEventArgs pointerEventArgs)
+ {
+ HoveredPin = Instance.OutputPin;
+ }
+
+ private void PinArea_MouseLeave(object? sender, PointerEventArgs pointerEventArgs)
+ {
+ HoveredPin = null;
+ }
+
+ public override Point GetPinOffset(NodeView card, int pinIndex) =>
+ PinArea.TranslatePoint(new Point(14, 8), card) ?? new Point(0, 0);
+
+ public override void UpdatePinIcon()
+ {
+ if (Instance.OutputPin.TargetList.Count == 0) Icon_Pin.Content = PinIconManager.Instance.ExecutePin_Null;
+ else Icon_Pin.Content = PinIconManager.Instance.ExecutePin;
+ }
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Control/ControlPinGroupView.axaml b/AXNode/SubSystem/NodeEditSystem/Control/ControlPinGroupView.axaml
new file mode 100644
index 0000000..920ce11
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Control/ControlPinGroupView.axaml
@@ -0,0 +1,10 @@
+
+
+
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Control/ControlPinGroupView.axaml.cs b/AXNode/SubSystem/NodeEditSystem/Control/ControlPinGroupView.axaml.cs
new file mode 100644
index 0000000..58ec31e
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Control/ControlPinGroupView.axaml.cs
@@ -0,0 +1,22 @@
+using System.Windows;
+using AXNode.SubSystem.NodeEditSystem.Control;
+using XLib.Node;
+
+namespace AXNode.SubSystem.NodeEditSystem.Control
+{
+ ///
+ /// 控件引脚组视图
+ ///
+ public partial class ControlPinGroupView : PinGroupViewBase
+ {
+ public ControlPinGroupView() => InitializeComponent();
+
+ public ControlPinGroup? Instance { get; set; } = null;
+
+ public override void Init()
+ {
+ if (Instance == null) return;
+ ControlBox.Children.Add((Avalonia.Controls.Control)Instance.ControlInstance);
+ }
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Control/DataPinGroupView.axaml b/AXNode/SubSystem/NodeEditSystem/Control/DataPinGroupView.axaml
new file mode 100644
index 0000000..f3d019d
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Control/DataPinGroupView.axaml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Control/DataPinGroupView.axaml.cs b/AXNode/SubSystem/NodeEditSystem/Control/DataPinGroupView.axaml.cs
new file mode 100644
index 0000000..547d4cd
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Control/DataPinGroupView.axaml.cs
@@ -0,0 +1,148 @@
+using System;
+using System.Windows;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Shapes;
+using Avalonia.Input;
+using Avalonia.Media;
+using Avalonia.Threading;
+using XLib.Node;
+using AXNode.SubSystem.NodeEditSystem.Define;
+using AXNode.SubSystem.ResourceSystem;
+
+namespace AXNode.SubSystem.NodeEditSystem.Control
+{
+ public partial class DataPinGroupView : PinGroupViewBase
+ {
+ #region 构造方法
+
+ public DataPinGroupView() => InitializeComponent();
+
+ #endregion
+
+ #region 属性
+
+ public DataPinGroup? Instance { get; set; } = null;
+
+ #endregion
+
+ #region 基类方法
+
+ public override void Init()
+ {
+ if (Instance == null) return;
+
+ Block_Name.Text = Instance.Name;
+ // Block_Name.Text = Instance.Name + GetTypeName();
+ Block_Name.Foreground = new SolidColorBrush(GetDataPinColor());
+ Input_Value.Text = Instance.Value;
+ Input_Value.IsReadOnly = !Instance.CanInput;
+ // InputBoxArea.Width = new GridLength(Instance.BoxWidth);
+
+ // 无输入引脚:隐藏引脚图标与区域
+ if (Instance.InputPin == null)
+ {
+ Icon_LeftPin.IsVisible = false;
+ LeftPinArea.IsVisible = false;
+ }
+ // 设置图标
+ else
+ {
+ Icon_LeftPin.Content = GetDataPinIcon();
+ LeftPinArea.PointerEntered += LeftPinArea_MouseEnter;
+ LeftPinArea.PointerExited += PinArea_MouseLeave;
+ }
+
+ // 无输出引脚:隐藏引脚图标与区域
+ if (Instance.OutputPin == null)
+ {
+ Icon_RightPin.IsVisible = false;
+ RightPinArea.IsVisible = false;
+ }
+ // 设置图标
+ else
+ {
+ Icon_RightPin.Content = GetDataPinIcon();
+ RightPinArea.PointerEntered += RightPinArea_MouseEnter;
+ RightPinArea.PointerExited += PinArea_MouseLeave;
+ }
+
+ Instance.ValueChanged += ValueChanged;
+ Input_Value.TextChanged += Input_Value_TextChanged;
+ }
+
+ public override Grid GetPinArea()
+ {
+ if (Instance.InputPin != null && HoveredPin == Instance.InputPin) return LeftPinArea;
+ if (Instance.OutputPin != null && HoveredPin == Instance.OutputPin) return RightPinArea;
+ throw new Exception("无命中引脚");
+ }
+
+ public override Point GetPinOffset(NodeView card, int pinIndex)
+ {
+ if (pinIndex == 0) return LeftPinArea.TranslatePoint(new Point(3, 8), card) ?? throw new Exception();
+ return RightPinArea.TranslatePoint(new Point(14, 8), card) ?? throw new Exception();
+ }
+
+ public override void UpdatePinIcon()
+ {
+ if (Instance.InputPin != null)
+ Icon_LeftPin.Content = GetDataPinIcon(Instance.InputPin.SourceList.Count > 0);
+ if (Instance.OutputPin != null)
+ Icon_RightPin.Content = GetDataPinIcon(Instance.OutputPin.TargetList.Count > 0);
+ }
+
+ #endregion
+
+ #region 控件事件
+
+ private void LeftPinArea_MouseEnter(object? sender, PointerEventArgs pointerEventArgs)
+ {
+ HoveredPin = Instance.InputPin;
+ }
+
+ private void RightPinArea_MouseEnter(object? sender, PointerEventArgs pointerEventArgs)
+ {
+ HoveredPin = Instance.OutputPin;
+ }
+
+ private void PinArea_MouseLeave(object? sender, PointerEventArgs pointerEventArgs)
+ {
+ HoveredPin = null;
+ }
+
+ #endregion
+
+ #region 私有方法
+
+ private Color GetDataPinColor()
+ {
+ return Instance.Type switch
+ {
+ "int" => PinColorSet.Int,
+ "double" => PinColorSet.Double,
+ "string" => PinColorSet.String,
+ "bool" => PinColorSet.Bool,
+ "byte[]" => PinColorSet.ByteArray,
+ _ => Colors.White,
+ };
+ }
+
+ private Shape GetDataPinIcon(bool solid = false)
+ {
+ return PinIconManager.Instance.GetDataPinIcon(Instance.Type, solid);
+ }
+
+ private void ValueChanged()
+ {
+ Dispatcher.UIThread.Invoke(() => Input_Value.Text = Instance.Value);
+ }
+
+ private void Input_Value_TextChanged(object sender, TextChangedEventArgs e)
+ {
+ Instance.Value = Input_Value.Text;
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Control/ExecutePinGroupView.axaml b/AXNode/SubSystem/NodeEditSystem/Control/ExecutePinGroupView.axaml
new file mode 100644
index 0000000..49436ec
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Control/ExecutePinGroupView.axaml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Control/ExecutePinGroupView.axaml.cs b/AXNode/SubSystem/NodeEditSystem/Control/ExecutePinGroupView.axaml.cs
new file mode 100644
index 0000000..8f0082d
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Control/ExecutePinGroupView.axaml.cs
@@ -0,0 +1,68 @@
+using System;
+using System.Windows;
+using System.Windows.Input;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using XLib.Node;
+using AXNode.SubSystem.ResourceSystem;
+
+namespace AXNode.SubSystem.NodeEditSystem.Control
+{
+ public partial class ExecutePinGroupView : PinGroupViewBase
+ {
+ public ExecutePinGroupView() => InitializeComponent();
+
+ public ExecutePinGroup? Instance { get; set; } = null;
+
+ public override void Init()
+ {
+ if (Instance == null) return;
+ Icon_LeftPin.Content = PinIconManager.Instance.ExecutePin_Null;
+ Block_ExecuteDesc.Text = Instance.ExecuteDesc;
+ Icon_RightPin.Content = PinIconManager.Instance.ExecutePin_Null;
+
+ LeftPinArea.PointerEntered += LeftPinArea_MouseEnter;
+ RightPinArea.PointerEntered += RightPinArea_MouseEnter;
+ LeftPinArea.PointerExited += PinArea_MouseLeave;
+ RightPinArea.PointerExited += PinArea_MouseLeave;
+ }
+
+ public override Grid GetPinArea()
+ {
+ if (HoveredPin == Instance.InputPin) return LeftPinArea;
+ if (HoveredPin == Instance.OutputPin) return RightPinArea;
+ throw new Exception("无命中引脚");
+ }
+
+ private void LeftPinArea_MouseEnter(object? sender, PointerEventArgs pointerEventArgs)
+ {
+ HoveredPin = Instance.InputPin;
+ }
+
+ private void RightPinArea_MouseEnter(object? sender, PointerEventArgs pointerEventArgs)
+ {
+ HoveredPin = Instance.OutputPin;
+ }
+
+ private void PinArea_MouseLeave(object? sender, PointerEventArgs pointerEventArgs)
+ {
+ HoveredPin = null;
+ }
+
+ public override Point GetPinOffset(NodeView card, int pinIndex)
+ {
+ if (pinIndex == 0) return LeftPinArea.TranslatePoint(new Point(3, 8), card) ?? throw new Exception();
+ return RightPinArea.TranslatePoint(new Point(14, 8), card) ?? throw new Exception();
+ }
+
+ public override void UpdatePinIcon()
+ {
+ if (Instance.InputPin.SourceList.Count == 0) Icon_LeftPin.Content = PinIconManager.Instance.ExecutePin_Null;
+ else Icon_LeftPin.Content = PinIconManager.Instance.ExecutePin;
+ if (Instance.OutputPin.TargetList.Count == 0)
+ Icon_RightPin.Content = PinIconManager.Instance.ExecutePin_Null;
+ else Icon_RightPin.Content = PinIconManager.Instance.ExecutePin;
+ }
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Control/HoverToolBar.axaml b/AXNode/SubSystem/NodeEditSystem/Control/HoverToolBar.axaml
new file mode 100644
index 0000000..e051725
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Control/HoverToolBar.axaml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Control/HoverToolBar.axaml.cs b/AXNode/SubSystem/NodeEditSystem/Control/HoverToolBar.axaml.cs
new file mode 100644
index 0000000..6def469
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Control/HoverToolBar.axaml.cs
@@ -0,0 +1,23 @@
+using System;
+using System.Windows;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+
+namespace AXNode.SubSystem.NodeEditSystem.Control
+{
+ public partial class HoverToolBar : UserControl
+ {
+ public HoverToolBar() => InitializeComponent();
+
+ public event Action ToolClick;
+
+ public void Init()
+ {
+ foreach (var item in Stack_ToolBar.Children)
+ if (item is Button button)
+ button.Click += Tool_Click;
+ }
+
+ private void Tool_Click(object sender, RoutedEventArgs e) => ToolClick?.Invoke(((Button)sender).Name);
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Control/Image/BreakPin.png b/AXNode/SubSystem/NodeEditSystem/Control/Image/BreakPin.png
new file mode 100644
index 0000000..2d1e72f
Binary files /dev/null and b/AXNode/SubSystem/NodeEditSystem/Control/Image/BreakPin.png differ
diff --git a/AXNode/SubSystem/NodeEditSystem/Control/Image/Delete.png b/AXNode/SubSystem/NodeEditSystem/Control/Image/Delete.png
new file mode 100644
index 0000000..f648450
Binary files /dev/null and b/AXNode/SubSystem/NodeEditSystem/Control/Image/Delete.png differ
diff --git a/AXNode/SubSystem/NodeEditSystem/Control/Image/Light_Black.png b/AXNode/SubSystem/NodeEditSystem/Control/Image/Light_Black.png
new file mode 100644
index 0000000..6c555aa
Binary files /dev/null and b/AXNode/SubSystem/NodeEditSystem/Control/Image/Light_Black.png differ
diff --git a/AXNode/SubSystem/NodeEditSystem/Control/Image/Light_Green.png b/AXNode/SubSystem/NodeEditSystem/Control/Image/Light_Green.png
new file mode 100644
index 0000000..3804816
Binary files /dev/null and b/AXNode/SubSystem/NodeEditSystem/Control/Image/Light_Green.png differ
diff --git a/AXNode/SubSystem/NodeEditSystem/Control/Image/Light_Red.png b/AXNode/SubSystem/NodeEditSystem/Control/Image/Light_Red.png
new file mode 100644
index 0000000..719b3b8
Binary files /dev/null and b/AXNode/SubSystem/NodeEditSystem/Control/Image/Light_Red.png differ
diff --git a/AXNode/SubSystem/NodeEditSystem/Control/Image/Start.png b/AXNode/SubSystem/NodeEditSystem/Control/Image/Start.png
new file mode 100644
index 0000000..2b0a7ff
Binary files /dev/null and b/AXNode/SubSystem/NodeEditSystem/Control/Image/Start.png differ
diff --git a/AXNode/SubSystem/NodeEditSystem/Control/Image/Stop.png b/AXNode/SubSystem/NodeEditSystem/Control/Image/Stop.png
new file mode 100644
index 0000000..d7e93b2
Binary files /dev/null and b/AXNode/SubSystem/NodeEditSystem/Control/Image/Stop.png differ
diff --git a/AXNode/SubSystem/NodeEditSystem/Control/NodeMap.axaml b/AXNode/SubSystem/NodeEditSystem/Control/NodeMap.axaml
new file mode 100644
index 0000000..87288d4
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Control/NodeMap.axaml
@@ -0,0 +1,12 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Control/NodeMap.axaml.cs b/AXNode/SubSystem/NodeEditSystem/Control/NodeMap.axaml.cs
new file mode 100644
index 0000000..f050d84
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Control/NodeMap.axaml.cs
@@ -0,0 +1,15 @@
+using Avalonia.Controls;
+
+namespace AXNode.SubSystem.NodeEditSystem.Control
+{
+ ///
+ /// NodeMap.xaml 的交互逻辑
+ ///
+ public partial class NodeMap : UserControl
+ {
+ public NodeMap()
+ {
+ InitializeComponent();
+ }
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Control/NodePropertyPanel.axaml b/AXNode/SubSystem/NodeEditSystem/Control/NodePropertyPanel.axaml
new file mode 100644
index 0000000..760a419
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Control/NodePropertyPanel.axaml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Control/NodePropertyPanel.axaml.cs b/AXNode/SubSystem/NodeEditSystem/Control/NodePropertyPanel.axaml.cs
new file mode 100644
index 0000000..19ca3ed
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Control/NodePropertyPanel.axaml.cs
@@ -0,0 +1,113 @@
+using System.Collections.Generic;
+using System.Windows;
+using System.Windows.Input;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Input;
+using AXNode.SubSystem.NodeEditSystem.Control.PropertyBar;
+using XLib.Node;
+
+namespace AXNode.SubSystem.NodeEditSystem.Control
+{
+ public partial class NodePropertyPanel : UserControl
+ {
+ public NodePropertyPanel()
+ {
+ InitializeComponent();
+ }
+
+ /// 节点实例
+ public NodeBase? Instance { get; set; }
+
+ ///
+ /// 加载属性条
+ ///
+ public void LoadPropertyBar()
+ {
+ if (Instance == null) return;
+
+ foreach (var property in Instance.PropertyList)
+ {
+ var bar = CreatePropertyBar(property);
+ if (bar == null) continue;
+
+ if (MainStackPanel.Children.Count > 0) bar.Margin = new Thickness(0, 10, 0, 0);
+ MainStackPanel.Children.Add(bar);
+ }
+ }
+
+ ///
+ /// 清空属性条
+ ///
+ public void ClearPropertyBar() => MainStackPanel.Children.Clear();
+
+ #region 控件事件
+
+ private void MainGrid_MouseWheel(object? sender, PointerWheelEventArgs e)
+ {
+ MainScrollBar.Value -= e.Delta.Y / 120 * 50;
+ }
+
+ private void MainStackPanel_SizeChanged(object sender, SizeChangedEventArgs e)
+ {
+ if (MainScrollBar != null)
+ {
+ MainScrollBar.ViewportSize = MainGrid.Height;
+ MainScrollBar.Maximum = MainStackPanel.Height - MainGrid.Height;
+ }
+ }
+
+ private void MainScrollBar_ValueChanged(object? sender,
+ RangeBaseValueChangedEventArgs rangeBaseValueChangedEventArgs)
+ {
+ MainStackPanel.Margin = new Thickness(0, -MainScrollBar.Value, 0, 0);
+ }
+
+ #endregion
+
+ #region 私有方法
+
+ ///
+ /// 创建属性条
+ ///
+ private PropertyBarBase? CreatePropertyBar(NodeProperty property)
+ {
+ PropertyBarBase? bar = null;
+
+ switch (property.Type)
+ {
+ case "int":
+ bar = new TextInput();
+ bar.Title = property.Name;
+ bar.LoadProperty(property.Value);
+ ((TextInput)bar).TextChanged += text => property.Value = text;
+ break;
+ case "label":
+ bar = new TextInput();
+ bar.Title = property.Name;
+ bar.LoadProperty(property.Value);
+ break;
+ case "List":
+ ListSelecter selecter = new ListSelecter();
+ bar = selecter;
+ List list = ((ListProperty)property).GetValueList();
+ selecter.Title = property.Name;
+ selecter.LoadProperty(list, property.Value);
+ selecter.SelectionChanged += index => ((ListProperty)property).Index = index;
+ break;
+ case "CustomList":
+ ListEditer editer = new ListEditer();
+ editer.Instance = (CustomListProperty)property;
+ editer.Init();
+ bar = editer;
+
+ break;
+ }
+
+ return bar;
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Control/NodeView.axaml b/AXNode/SubSystem/NodeEditSystem/Control/NodeView.axaml
new file mode 100644
index 0000000..599624d
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Control/NodeView.axaml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Control/NodeView.axaml.cs b/AXNode/SubSystem/NodeEditSystem/Control/NodeView.axaml.cs
new file mode 100644
index 0000000..b2eefc4
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Control/NodeView.axaml.cs
@@ -0,0 +1,360 @@
+using System;
+using System.Collections.Generic;
+using System.Windows;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Media;
+using Avalonia.Threading;
+using AXNode.SubSystem.ResourceSystem;
+using AXNode.SubSystem.TimerSystem;
+using XLib.Avalonia.UI;
+using XLib.Node;
+using XLib.Avalonia.UI;
+using XLib.AvaloniaControl;
+using AXNode.AppTool;
+using AXNode.SubSystem.NodeEditSystem.Define;
+using AXNode.SubSystem.ResourceSystem;
+using AXNode.SubSystem.TimerSystem;
+using AXNode.SubSystem.WindowSystem;
+
+// using ProgressBar = XLib.AvaloniaControl.ProgressBar;
+
+namespace AXNode.SubSystem.NodeEditSystem.Control
+{
+ public partial class NodeView : MoveableControl
+ {
+ #region 构造方法
+
+ public NodeView() => InitializeComponent();
+
+ #endregion
+
+ #region 属性
+
+ /// 节点颜色
+ public Color NodeColor
+ {
+ get => _nodeColor;
+ set
+ {
+ _nodeColor = value;
+ // Color_Start.Color = Color.FromArgb(255, _nodeColor.R, _nodeColor.G, _nodeColor.B);
+ // Color_End.Color = Color.FromArgb(0, _nodeColor.R, _nodeColor.G, _nodeColor.B);
+ NodeFillColor.Background =
+ new SolidColorBrush(Color.FromArgb(48, _nodeColor.R, _nodeColor.G, _nodeColor.B));
+ }
+ }
+
+ /// 节点实例
+ public NodeBase NodeInstance { get; set; }
+
+ ///
+ /// 悬停引脚
+ ///
+ public PinBase? HoveredPin
+ {
+ get
+ {
+ foreach (var item in _pinGroupViewList)
+ if (item.HoveredPin != null)
+ return item.HoveredPin;
+ return null;
+ }
+ }
+
+ #endregion
+
+ #region 事件
+
+ /// 节点背景.鼠标进入
+ public Action? NodeBackMouseEnter { get; set; } = null;
+
+ /// 节点背景.鼠标离开
+ public Action? NodeBackMouseLeave { get; set; } = null;
+
+ /// 引脚组列表变更
+ public Action? PinGroupListChanged { get; set; } = null;
+
+ /// 节点变更
+ public Action? NodeChanged { get; set; } = null;
+
+ #endregion
+
+ #region 公开方法
+
+ ///
+ /// 初始化
+ ///
+ public void Init()
+ {
+ // 设置节点图标与标题
+ NodeIcon.Source = ImageResManager.Instance.GetNodeIcon(NodeInstance.Icon);
+ Block_Title.Text = NodeInstance.Title;
+ // 加载引脚组
+ foreach (var pinGroup in NodeInstance.PinGroupList)
+ {
+ PinGroupViewBase? pinView = CreatePinGroupView(pinGroup);
+ if (pinView == null) continue;
+ Stack_PinGroupList.Children.Add(pinView);
+ _pinGroupViewList.Add(pinView);
+ pinView.Init();
+ }
+
+ if (_pinGroupViewList.Count > 0)
+ _pinGroupViewList[0].Margin = new Thickness(0);
+ // 设置节点委托
+ NodeInstance.OpenProgressBar = OpenProgressBar;
+ NodeInstance.CloseProgressBar = CloseProgress;
+ // 监听节点
+ NodeInstance.StateChanged += Node_StateChanged;
+ NodeInstance.ExecuteError += Node_ExecuteError;
+ NodeInstance.ParaChanged += Node_ParaChanged;
+ NodeInstance.PropertyChanged += Node_PropertyChanged;
+ NodeInstance.PinGroupListChanged += Node_PinGroupListChanged;
+ // 设置定时器处理器委托
+ _timerHandler.OnTick = TimerHandler_Tick;
+ }
+
+ ///
+ /// 清理
+ ///
+ public void Clear()
+ {
+ NodeInstance.StateChanged -= Node_StateChanged;
+ NodeInstance.ExecuteError -= Node_ExecuteError;
+ NodeInstance.ParaChanged -= Node_ParaChanged;
+ NodeInstance.PropertyChanged -= Node_PropertyChanged;
+ NodeInstance.PinGroupListChanged -= Node_PinGroupListChanged;
+ }
+
+ ///
+ /// 获取悬停引脚的偏移
+ ///
+ public Point GetHoveredPinOffset()
+ {
+ foreach (var groupView in _pinGroupViewList)
+ {
+ if (groupView.HoveredPin != null)
+ return groupView.GetHoveredPinOffset();
+ }
+
+ return new Point();
+ }
+
+ ///
+ /// 获取引脚相对于节点的坐标
+ ///
+ public Point GetPinOffset(PinPath path) => _pinGroupViewList[path.GroupIndex].GetPinOffset(this, path.PinIndex);
+
+ ///
+ /// 更新全部引脚图标
+ ///
+ public void UpdateAllPinIcon()
+ {
+ foreach (var groupCard in _pinGroupViewList) groupCard.UpdatePinIcon();
+ }
+
+ ///
+ /// 获取可命中矩形区域
+ ///
+ public Rect GetHittableRect() =>
+ new Rect(Canvas.GetLeft(this) + 11, Canvas.GetTop(this), Bounds.Width - 22, Bounds.Height);
+
+ #endregion
+
+ #region MoveableControl 方法
+
+ protected override void OnOffsetChanged() => NodeInstance.Point = new NodePoint((int)Point.X, (int)Point.Y);
+
+ #endregion
+
+ #region 控件事件
+
+ private void NodeBack_MouseEnter(object? sender, PointerEventArgs pointerEventArgs)
+ {
+ NodeBackMouseEnter?.Invoke(this);
+ }
+
+ private void NodeBack_MouseLeave(object? sender, PointerEventArgs pointerEventArgs)
+ {
+ NodeBackMouseLeave?.Invoke(this);
+ }
+
+ #endregion
+
+ #region 节点委托
+
+ private void OpenProgressBar(IProgressGetter progressGetter)
+ {
+ // 添加进度条控件
+ Dispatcher.UIThread.Invoke(() =>
+ {
+ // _progressBar = new ProgressBar
+ // {
+ // Height = 1,
+ // VerticalAlignment = VerticalAlignment.Bottom,
+ // IsHitTestVisible = false
+ // };
+ NodeBack.Children.Add(_progressBar);
+ Grid.SetRow(_progressBar, 1);
+ });
+ // 设置进度获取器
+ _progressGetter = progressGetter;
+ // 启动实时刷新
+ AppTimer.Instance.AddTimerHandler(_timerHandler);
+ }
+
+ private void CloseProgress()
+ {
+ // 停止刷新
+ AppTimer.Instance.RemoveTimerHandler(_timerHandler);
+ // 移除进度条控件
+ Dispatcher.UIThread.Invoke(() => NodeBack.Children.Remove(_progressBar));
+ _progressBar = null;
+ }
+
+ #endregion
+
+ #region 节点事件
+
+ private void Node_StateChanged()
+ {
+ AppDelegate.Invoke(() =>
+ {
+ if (NodeInstance.State == NodeState.Enable)
+ Image_Light.Source =
+ ImageResManager.Instance.GetSubSystemImage("NodeEditSystem", "Control/Image", "Light_Green");
+ else
+ {
+ if (NodeInstance.RunError)
+ Image_Light.Source =
+ ImageResManager.Instance.GetSubSystemImage("NodeEditSystem", "Control/Image", "Light_Red");
+ else
+ Image_Light.Source =
+ ImageResManager.Instance.GetSubSystemImage("NodeEditSystem", "Control/Image",
+ "Light_Black");
+ }
+ });
+ }
+
+ private void Node_ExecuteError(Exception ex)
+ {
+ Dispatcher.UIThread.Invoke(() => WM.ShowError("节点执行异常:" + ex.Message));
+ }
+
+ private void Node_ParaChanged() => NodeChanged?.Invoke();
+
+ private void Node_PropertyChanged() => NodeChanged?.Invoke();
+
+ private void Node_PinGroupListChanged()
+ {
+ // 清空当前引脚组
+ Stack_PinGroupList.Children.Clear();
+ _pinGroupViewList.Clear();
+ // 加载引脚组
+ foreach (var pinGroup in NodeInstance.PinGroupList)
+ {
+ PinGroupViewBase? pinView = CreatePinGroupView(pinGroup);
+ if (pinView == null) continue;
+ Stack_PinGroupList.Children.Add(pinView);
+ _pinGroupViewList.Add(pinView);
+ pinView.Init();
+ }
+
+ if (_pinGroupViewList.Count > 0)
+ _pinGroupViewList[0].Margin = new Thickness(0);
+
+ UpdateLayout();
+ // 通知引脚组列表变更
+ PinGroupListChanged?.Invoke();
+ // 通知节点变更
+ NodeChanged?.Invoke();
+ }
+
+ ///
+ /// 定时器.走动
+ ///
+ private void TimerHandler_Tick()
+ {
+ if (_progressBar == null || _progressGetter == null) return;
+ // _progressBar.Progress = _progressGetter.GetProgress();
+ }
+
+ ///
+ /// 查找引脚组
+ ///
+ private PinGroupBase? FindPinGroup(string title)
+ {
+ foreach (var pinGroup in NodeInstance.PinGroupList)
+ if (pinGroup.GetTitle() == title)
+ return pinGroup;
+ return null;
+ }
+
+ #endregion
+
+ #region 私有方法
+
+ ///
+ /// 创建引脚组视图
+ ///
+ private PinGroupViewBase? CreatePinGroupView(PinGroupBase pinGroup)
+ {
+ switch (pinGroup.GroupType)
+ {
+ case PinGroupType.Execute:
+ return new ExecutePinGroupView
+ {
+ Instance = (ExecutePinGroup)pinGroup,
+ Margin = new Thickness(0, _pinItemInterval, 0, 0),
+ };
+ case PinGroupType.Data:
+ return new DataPinGroupView
+ {
+ Instance = (DataPinGroup)pinGroup,
+ Margin = new Thickness(0, _pinItemInterval, 0, 0),
+ };
+ case PinGroupType.Action:
+ return new ActionPinGroupView
+ {
+ Instance = (ActionPinGroup)pinGroup,
+ Margin = new Thickness(0, _pinItemInterval, 0, 0),
+ };
+ case PinGroupType.Control:
+ return new ControlPinGroupView
+ {
+ Margin = new Thickness(0, _pinItemInterval, 0, 0),
+ Instance = (ControlPinGroup)pinGroup,
+ };
+ }
+
+ return null;
+ }
+
+ #endregion
+
+ #region 字段
+
+ private readonly int _pinItemInterval = 5;
+
+ /// 引脚组列表
+ private readonly List _pinGroupViewList = new List();
+
+ /// 引脚信息列表
+ private readonly List _connectInfoList = new List();
+
+ private Color _nodeColor = Colors.White;
+
+ /// 进度条控件
+ private ProgressBar? _progressBar = null;
+
+ /// 进度获取器
+ private IProgressGetter? _progressGetter = null;
+
+ /// 定时器处理器
+ private readonly TimerHandler _timerHandler = new TimerHandler();
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Control/PinGroupViewBase.cs b/AXNode/SubSystem/NodeEditSystem/Control/PinGroupViewBase.cs
new file mode 100644
index 0000000..d276f17
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Control/PinGroupViewBase.cs
@@ -0,0 +1,62 @@
+using System;
+using Avalonia;
+using Avalonia.Controls;
+using XLib.AvaloniaControl;
+using XLib.Node;
+
+namespace AXNode.SubSystem.NodeEditSystem.Control
+{
+ ///
+ /// 引脚组视图基类
+ ///
+ public class PinGroupViewBase : UserControl
+ {
+ /// 悬停引脚
+ public PinBase? HoveredPin { get; set; } = null;
+
+ ///
+ /// 初始化
+ ///
+ public virtual void Init()
+ {
+ }
+
+ ///
+ /// 获取引脚区域控件
+ ///
+ public virtual Grid GetPinArea() => throw new NotImplementedException();
+
+ ///
+ /// 获取悬停引脚相对于鼠标的偏移量
+ ///
+ public Point GetHoveredPinOffset()
+ {
+ Grid pinArea = GetPinArea();
+ Point offset = Mouse.GetPosition(pinArea);
+ if (HoveredPin.Flow == PinFlow.Input)
+ {
+ offset = new(3 - offset.X, offset.Y);
+ offset = new(offset.X, 8 - offset.Y);
+ }
+ else
+ {
+ offset = new(14 - offset.X, offset.Y);
+ offset = new(offset.X, 8 - offset.Y);
+ }
+
+ return offset;
+ }
+
+ ///
+ /// 获取相对于节点的坐标
+ ///
+ public virtual Point GetPinOffset(NodeView card, int pinIndex) => new Point();
+
+ ///
+ /// 更新引脚图标
+ ///
+ public virtual void UpdatePinIcon()
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Control/PropertyBar/CustomListItem.axaml b/AXNode/SubSystem/NodeEditSystem/Control/PropertyBar/CustomListItem.axaml
new file mode 100644
index 0000000..9f06b24
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Control/PropertyBar/CustomListItem.axaml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Control/PropertyBar/CustomListItem.axaml.cs b/AXNode/SubSystem/NodeEditSystem/Control/PropertyBar/CustomListItem.axaml.cs
new file mode 100644
index 0000000..508e01d
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Control/PropertyBar/CustomListItem.axaml.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Windows;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+
+namespace AXNode.SubSystem.NodeEditSystem.Control.PropertyBar
+{
+ public partial class CustomListItem : UserControl
+ {
+ public CustomListItem()
+ {
+ InitializeComponent();
+ }
+
+ public string ItemValue
+ {
+ get => Input_Value.Text;
+ set => Input_Value.Text = value;
+ }
+
+ public Action? Remove_Click { get; set; } = null;
+
+ public Action? Value_Changed { get; set; } = null;
+
+ private void Input_Value_TextChanged(object sender, TextChangedEventArgs e) => Value_Changed?.Invoke(this);
+
+ private void Tool_Remove_Click(object sender, RoutedEventArgs e) => Remove_Click?.Invoke(this);
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Control/PropertyBar/ListEditer.axaml b/AXNode/SubSystem/NodeEditSystem/Control/PropertyBar/ListEditer.axaml
new file mode 100644
index 0000000..23adbda
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Control/PropertyBar/ListEditer.axaml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Control/PropertyBar/ListEditer.axaml.cs b/AXNode/SubSystem/NodeEditSystem/Control/PropertyBar/ListEditer.axaml.cs
new file mode 100644
index 0000000..ced08c4
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Control/PropertyBar/ListEditer.axaml.cs
@@ -0,0 +1,96 @@
+using System.Windows;
+using Avalonia;
+using Avalonia.Interactivity;
+using XLib.Base;
+using XLib.Node;
+
+namespace AXNode.SubSystem.NodeEditSystem.Control.PropertyBar
+{
+ public partial class ListEditer : PropertyBarBase
+ {
+ public ListEditer() => InitializeComponent();
+
+ public CustomListProperty Instance { get; set; }
+
+ public void Init()
+ {
+ Stack_CaseList.Children.Clear();
+ foreach (var item in Instance.ItemList)
+ {
+ CustomListItem listItem = new CustomListItem
+ {
+ ItemValue = item,
+ Margin = new Thickness(0, 10, 0, 0),
+ Remove_Click = OnRemoveClick,
+ Value_Changed = OnValueChanged
+ };
+ Stack_CaseList.Children.Add(listItem);
+ }
+
+ if (Stack_CaseList.Children.Count > 0)
+ ((CustomListItem)Stack_CaseList.Children[0]).Margin = new Thickness();
+ else Grid_Tool.Margin = new Thickness();
+ }
+
+ private void Tool_Add_Click(object? sender, RoutedEventArgs routedEventArgs)
+ {
+ (int, int) mousePoint = OSTool.GetMousePoint();
+
+ // 生成项
+ string value = GenerateCaseValue();
+ CustomListItem listItem = new CustomListItem
+ {
+ ItemValue = value,
+ Margin = new Thickness(0, 10, 0, 0),
+ Remove_Click = OnRemoveClick,
+ Value_Changed = OnValueChanged
+ };
+ // 添加项
+ Stack_CaseList.Children.Add(listItem);
+ ((CustomListItem)Stack_CaseList.Children[0]).Margin = new Thickness();
+ Instance.ItemList.Add(value);
+ Instance.ItemAdded?.Invoke(value);
+
+ Grid_Tool.Margin = new Thickness(0, 10, 0, 0);
+
+ mousePoint.Item2 += 35;
+ OSTool.SetMousePoint(mousePoint.Item1, mousePoint.Item2);
+ }
+
+ private void OnRemoveClick(CustomListItem listItem)
+ {
+ // 移除项
+ Stack_CaseList.Children.Remove(listItem);
+ if (Stack_CaseList.Children.Count > 0)
+ ((CustomListItem)Stack_CaseList.Children[0]).Margin = new Thickness();
+ else Grid_Tool.Margin = new Thickness();
+
+ int index = Instance.ItemList.IndexOf(listItem.ItemValue);
+ Instance.ItemList.Remove(listItem.ItemValue);
+ Instance.ItemRemoved?.Invoke(index);
+ }
+
+ private void OnValueChanged(CustomListItem listItem)
+ {
+ int index = Stack_CaseList.Children.IndexOf(listItem);
+ Instance.ItemList[index] = listItem.ItemValue;
+ Instance.ItemChanged?.Invoke(index, listItem.ItemValue);
+ }
+
+ private string GenerateCaseValue()
+ {
+ int nameID = 1;
+ while (true)
+ {
+ string name = $"Case_{nameID:00}";
+ if (Instance.ItemList.Contains(name))
+ {
+ nameID++;
+ continue;
+ }
+
+ return name;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Control/PropertyBar/ListSelecter.axaml b/AXNode/SubSystem/NodeEditSystem/Control/PropertyBar/ListSelecter.axaml
new file mode 100644
index 0000000..cfcb849
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Control/PropertyBar/ListSelecter.axaml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Control/PropertyBar/ListSelecter.axaml.cs b/AXNode/SubSystem/NodeEditSystem/Control/PropertyBar/ListSelecter.axaml.cs
new file mode 100644
index 0000000..7cf23d9
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Control/PropertyBar/ListSelecter.axaml.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+using System.Windows.Input;
+using Avalonia.Controls;
+using Avalonia.Input;
+
+namespace AXNode.SubSystem.NodeEditSystem.Control.PropertyBar
+{
+ public partial class ListSelecter : PropertyBarBase
+ {
+ public ListSelecter() => InitializeComponent();
+
+ public Action? SelectionChanged { get; set; } = null;
+
+ public void LoadProperty(List list, string value)
+ {
+ Block_Title.Text = Title;
+ foreach (var item in list)
+ {
+ ComboBoxItem boxItem = new ComboBoxItem
+ {
+ Content = item,
+ };
+ ToolTip.SetTip(boxItem, item);
+ Box_ItemList.Items.Add(boxItem);
+ }
+
+ Box_ItemList.SelectedIndex = list.IndexOf(value);
+ }
+
+ private void MainGrid_MouseEnter(object? sender, PointerEventArgs pointerEventArgs)
+ {
+ Block_Title.Foreground = _hovered;
+ }
+
+ private void MainGrid_MouseLeave(object? sender, PointerEventArgs pointerEventArgs)
+ {
+ Block_Title.Foreground = _default;
+ }
+
+ private void Box_ItemList_SelectionChanged(object? sender, SelectionChangedEventArgs selectionChangedEventArgs)
+ {
+ SelectionChanged?.Invoke(Box_ItemList.SelectedIndex - 1);
+ }
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Control/PropertyBar/PropertyBarBase.cs b/AXNode/SubSystem/NodeEditSystem/Control/PropertyBar/PropertyBarBase.cs
new file mode 100644
index 0000000..6a1dc75
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Control/PropertyBar/PropertyBarBase.cs
@@ -0,0 +1,17 @@
+using Avalonia.Controls;
+using Avalonia.Media;
+
+namespace AXNode.SubSystem.NodeEditSystem.Control.PropertyBar
+{
+ public class PropertyBarBase : UserControl
+ {
+ public string Title { get; set; } = "";
+
+ public virtual void LoadProperty(string value)
+ {
+ }
+
+ protected SolidColorBrush _default = new SolidColorBrush(Color.FromRgb(140, 140, 140));
+ protected SolidColorBrush _hovered = new SolidColorBrush(Color.FromRgb(255, 255, 255));
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Control/PropertyBar/TextInput.axaml b/AXNode/SubSystem/NodeEditSystem/Control/PropertyBar/TextInput.axaml
new file mode 100644
index 0000000..ae63dcb
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Control/PropertyBar/TextInput.axaml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Control/PropertyBar/TextInput.axaml.cs b/AXNode/SubSystem/NodeEditSystem/Control/PropertyBar/TextInput.axaml.cs
new file mode 100644
index 0000000..9d66588
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Control/PropertyBar/TextInput.axaml.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Windows.Input;
+using Avalonia.Controls;
+using Avalonia.Input;
+
+namespace AXNode.SubSystem.NodeEditSystem.Control.PropertyBar
+{
+ public partial class TextInput : PropertyBarBase
+ {
+ public TextInput()
+ {
+ InitializeComponent();
+ // Input_Value.TextChanged += OnTextChanged;
+ Input_Value.KeyDown += Input_Value_KeyDown;
+ }
+
+ public string Text { get; set; } = "";
+
+ public event Action TextChanged;
+
+ ///
+ /// 加载属性
+ ///
+ public override void LoadProperty(string text)
+ {
+ Block_Title.Text = Title;
+ Input_Value.Text = text;
+ }
+
+ private void MainGrid_MouseEnter(object? sender, PointerEventArgs pointerEventArgs)
+ {
+ Block_Title.Foreground = _hovered;
+ }
+
+ private void MainGrid_MouseLeave(object? sender, PointerEventArgs pointerEventArgs)
+ {
+ Block_Title.Foreground = _default;
+ }
+
+ private void OnTextChanged(object sender, TextChangedEventArgs e)
+ {
+ TextChanged?.Invoke(Input_Value.Text);
+ Text = Input_Value.Text;
+ }
+
+ private void Input_Value_KeyDown(object sender, KeyEventArgs e)
+ {
+ if (e.Key == Key.Enter)
+ {
+ e.Handled = true;
+ TextChanged?.Invoke(Input_Value.Text);
+ Text = Input_Value.Text;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Define/ConnectLine.cs b/AXNode/SubSystem/NodeEditSystem/Define/ConnectLine.cs
new file mode 100644
index 0000000..27e34b4
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Define/ConnectLine.cs
@@ -0,0 +1,15 @@
+using System.Windows;
+using Avalonia;
+
+namespace AXNode.SubSystem.NodeEditSystem.Define
+{
+ ///
+ /// 连接线
+ ///
+ public class ConnectLine
+ {
+ public Point Start { get; set; }
+
+ public Point End { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Define/EnumDefine.cs b/AXNode/SubSystem/NodeEditSystem/Define/EnumDefine.cs
new file mode 100644
index 0000000..b7b3f23
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Define/EnumDefine.cs
@@ -0,0 +1,32 @@
+namespace AXNode.SubSystem.NodeEditSystem.Define
+{
+ ///
+ /// 鼠标命中区域
+ ///
+ public enum MouseHitedArea
+ {
+ /// 空白处
+ Space,
+
+ /// 节点
+ Node,
+
+ /// 引脚
+ Pin,
+
+ /// 连接线
+ ConnectLine,
+ }
+
+ ///
+ /// 选择方式
+ ///
+ public enum SelectType
+ {
+ /// 框选
+ Box,
+
+ /// 交叉
+ Cross
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Define/PinColorSet.cs b/AXNode/SubSystem/NodeEditSystem/Define/PinColorSet.cs
new file mode 100644
index 0000000..761a1b7
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Define/PinColorSet.cs
@@ -0,0 +1,23 @@
+using Avalonia.Media;
+using XLib.Avalonia.Ex;
+
+namespace AXNode.SubSystem.NodeEditSystem.Define
+{
+ ///
+ /// 引脚颜色集
+ ///
+ public class PinColorSet
+ {
+ public static Color Execute => "C47EFF".ToColor();
+
+ public static Color Bool => "A7C4B5".ToColor();
+
+ public static Color Int => "B3D465".ToColor();
+
+ public static Color Double => "E06C9F".ToColor();
+
+ public static Color String => "F3B562".ToColor();
+
+ public static Color ByteArray => "6CB891".ToColor();
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Define/PinConnectInfo.cs b/AXNode/SubSystem/NodeEditSystem/Define/PinConnectInfo.cs
new file mode 100644
index 0000000..5934ed4
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Define/PinConnectInfo.cs
@@ -0,0 +1,17 @@
+using System.Collections.Generic;
+using XLib.Node;
+
+namespace AXNode.SubSystem.NodeEditSystem.Define
+{
+ ///
+ /// 引脚连接信息
+ ///
+ public class PinConnectInfo
+ {
+ public string Title { get; set; } = "";
+
+ public PinFlow Flow { get; set; } = PinFlow.Input;
+
+ public List PathList { get; set; } = new List();
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Define/PinPath.cs b/AXNode/SubSystem/NodeEditSystem/Define/PinPath.cs
new file mode 100644
index 0000000..191a87a
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Define/PinPath.cs
@@ -0,0 +1,33 @@
+namespace AXNode.SubSystem.NodeEditSystem.Define
+{
+ ///
+ /// 引脚路径:节点编号.引脚组索引.引脚索引
+ ///
+ public class PinPath
+ {
+ ///
+ /// 解析引脚路径
+ ///
+ public static PinPath ParsePinPath(string path)
+ {
+ string[] nodeArray = path.Split(',');
+ return new PinPath
+ {
+ NodeVersion = nodeArray[0],
+ NodeID = int.Parse(nodeArray[1]),
+ GroupIndex = int.Parse(nodeArray[2]),
+ PinIndex = int.Parse(nodeArray[3])
+ };
+ }
+
+ public string NodeVersion { get; set; } = "1.0";
+
+ public int NodeID { get; set; } = -1;
+
+ public int GroupIndex { get; set; } = -1;
+
+ public int PinIndex { get; set; } = -1;
+
+ public override string ToString() => $"{NodeVersion},{NodeID},{GroupIndex},{PinIndex}";
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Define/TargetBox.cs b/AXNode/SubSystem/NodeEditSystem/Define/TargetBox.cs
new file mode 100644
index 0000000..d8abc54
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Define/TargetBox.cs
@@ -0,0 +1,66 @@
+using System.Collections.Generic;
+using System.Windows;
+using Avalonia;
+
+namespace AXNode.SubSystem.NodeEditSystem.Define
+{
+ ///
+ /// 目标框
+ ///
+ public class TargetBox
+ {
+ public Point ScreenPoint { get; set; }
+
+ public double Height { get; set; }
+
+ public double Width { get; set; }
+
+ /// 外框偏移。正数向外,负数向内
+ public double BoxOffset { get; set; } = 0;
+
+ ///
+ /// 获取绘制目标框的坐标列表。共绘制八条线,每条线两个坐标
+ ///
+ public List GetPointList(int lineLength)
+ {
+ List result = new List();
+
+ double left = ScreenPoint.X - BoxOffset;
+ double right = ScreenPoint.X + Width + BoxOffset;
+ double top = ScreenPoint.Y - BoxOffset;
+ double bottom = ScreenPoint.Y + Height + BoxOffset;
+
+ double hx1 = left + lineLength;
+ double hx2 = right - lineLength;
+ double hy1 = top + 0.5;
+ double hy2 = bottom - 0.5;
+
+ result.Add(new Point(left, hy1));
+ result.Add(new Point(hx1, hy1));
+ result.Add(new Point(hx2, hy1));
+ result.Add(new Point(right, hy1));
+
+ result.Add(new Point(left, hy2));
+ result.Add(new Point(hx1, hy2));
+ result.Add(new Point(hx2, hy2));
+ result.Add(new Point(right, hy2));
+
+ double vx1 = left + 0.5;
+ double vx2 = right - 0.5;
+ double vy1 = top + lineLength;
+ double vy2 = bottom - lineLength;
+
+ result.Add(new Point(vx1, top));
+ result.Add(new Point(vx1, vy1));
+ result.Add(new Point(vx1, vy2));
+ result.Add(new Point(vx1, bottom));
+
+ result.Add(new Point(vx2, top));
+ result.Add(new Point(vx2, vy1));
+ result.Add(new Point(vx2, vy2));
+ result.Add(new Point(vx2, bottom));
+
+ return result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Panel/Component/CardComponent.cs b/AXNode/SubSystem/NodeEditSystem/Panel/Component/CardComponent.cs
new file mode 100644
index 0000000..6d91bee
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Panel/Component/CardComponent.cs
@@ -0,0 +1,141 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Windows;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+using XLib.Base.UIComponent;
+using XLib.Node;
+using AXNode.SubSystem.NodeEditSystem.Control;
+
+namespace AXNode.SubSystem.NodeEditSystem.Panel.Component
+{
+ ///
+ /// 卡片组件:管理编辑器中的节点控件
+ ///
+ public class CardComponent : Component
+ {
+ #region 属性
+
+ /// 全部卡片
+ public List AllCard => _cardList;
+
+ /// 选中卡片列表
+ public List SelectedCardList => _selectedCardSet.ToList();
+
+ #endregion
+
+ #region 生命周期
+
+ protected override void Reset()
+ {
+ _host.LayerBox_Node.Children.Clear();
+ _cardList.Clear();
+ _selectedCardSet.Clear();
+ }
+
+ #endregion
+
+ #region 公开方法
+
+ ///
+ /// 生成节点卡片
+ ///
+ public NodeView GenerateNodeCard(NodeBase node)
+ {
+ // 创建节点卡片
+ NodeView card = new NodeView
+ {
+ NodeColor = Color.FromRgb(node.Color.r, node.Color.g, node.Color.b),
+ NodeInstance = node,
+ Point = new Point(node.Point.X, node.Point.Y),
+ };
+ // 设置卡片坐标
+ Point center = GetComponent().WorldCenter;
+ Canvas.SetLeft(card, center.X + node.Point.X - 12);
+ Canvas.SetTop(card, center.Y + node.Point.Y - 1);
+ // 初始化节点卡片
+ card.Init();
+ // 添加视图
+ _host.LayerBox_Node.Children.Add(card);
+ _cardList.Add(card);
+ // 调用已加载
+ node.Loaded();
+ // 返回节点视图
+ return card;
+ }
+
+ ///
+ /// 添加选择
+ ///
+ public void AddSelect(NodeView view) => _selectedCardSet.Add(view);
+
+ ///
+ /// 添加选择
+ ///
+ public void RemoveSelect(NodeView view) => _selectedCardSet.Remove(view);
+
+ ///
+ /// 清空选择
+ ///
+ public void ClearSelect() => _selectedCardSet.Clear();
+
+ ///
+ /// 更新节点卡片
+ ///
+ public void UpdateNodeCard()
+ {
+ Point center = GetComponent().WorldCenter;
+ foreach (var card in _cardList)
+ {
+ Canvas.SetLeft(card, center.X + card.Point.X - 12);
+ Canvas.SetTop(card, center.Y + card.Point.Y - 1);
+ }
+ }
+
+ ///
+ /// 将卡片置顶
+ ///
+ public void SetTop(NodeView view)
+ {
+ _host.LayerBox_Node.Children.Remove(view);
+ _host.LayerBox_Node.Children.Add(view);
+ _cardList.Remove(view);
+ _cardList.Add(view);
+ }
+
+ ///
+ /// 获取节点卡片
+ ///
+ public NodeView GetNodeCard(int nodeID)
+ {
+ foreach (var card in _cardList)
+ if (card.NodeInstance.ID == nodeID)
+ return card;
+ throw new Exception("获取节点卡片失败");
+ }
+
+ ///
+ /// 删除节点卡片
+ ///
+ public void DeleteNodeCard(NodeView card)
+ {
+ card.Clear();
+ _host.LayerBox_Node.Children.Remove(card);
+ _cardList.Remove(card);
+ }
+
+ #endregion
+
+ #region 字段
+
+ /// 卡片列表
+ private readonly List _cardList = new List();
+
+ /// 选中卡片集
+ private readonly HashSet _selectedCardSet = new HashSet();
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Panel/Component/DrawingComponent.cs b/AXNode/SubSystem/NodeEditSystem/Panel/Component/DrawingComponent.cs
new file mode 100644
index 0000000..47224c8
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Panel/Component/DrawingComponent.cs
@@ -0,0 +1,403 @@
+using System;
+using System.Windows;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+using AXNode.AppTool;
+using XLib.Animate;
+using XLib.Base.UIComponent;
+using XLib.Math.Easing;
+using XLib.Node;
+using AXNode.SubSystem.NodeEditSystem.Control;
+using AXNode.SubSystem.NodeEditSystem.Define;
+using AXNode.SubSystem.NodeEditSystem.Panel.Layer;
+using XLib.Avalonia.Drawing;
+using AXNode.SubSystem.NodeEditSystem.Define;
+using AXNode.SubSystem.NodeEditSystem.Panel.Layer;
+
+namespace AXNode.SubSystem.NodeEditSystem.Panel.Component
+{
+ public class DrawingComponent : Component
+ {
+ #region 属性
+
+ /// 世界中心
+ public Point WorldCenter => _gridLayer.GridCenter;
+
+ /// 悬停框
+ public TargetBox? HoverBox
+ {
+ get => _hoverBoxLayer?.Box;
+ set
+ {
+ // 图层为空,则忽略
+ if (_hoverBoxLayer == null) return;
+
+ // 设置悬停框
+ _hoverBoxLayer.Box = value;
+ // 播放悬停框动画
+ if (value != null)
+ _hoverBoxLayer.Motion("BoxMargin", 10, 0, 200, EasingType.QuinticEase, EasingMode.EaseOut);
+ }
+ }
+
+ /// 悬停的连接线
+ public VisualConnectLine? HoveredConnectLine => _hoveredLine as VisualConnectLine;
+
+ #endregion
+
+ #region 生命周期
+
+ protected override void Init()
+ {
+ EnableLayer();
+ _host.OperateArea.SizeChanged += OperateArea_SizeChanged;
+ }
+
+ protected override void Reset() => ResetLayer();
+
+ #endregion
+
+ #region 公开方法
+
+ ///
+ /// 屏幕坐标转世界坐标
+ ///
+ public Point ScreenToWorld(Point screenPoint)
+ {
+ // 转世界坐标
+ Point worldPoint = new Point(screenPoint.X - _gridLayer.GridCenter.X,
+ screenPoint.Y - _gridLayer.GridCenter.Y);
+ // 对齐至网格
+ double x = Math.Round(worldPoint.X / _gridLayer.CellWidth) * _gridLayer.CellWidth;
+ double y = Math.Round(worldPoint.Y / _gridLayer.CellHeight) * _gridLayer.CellHeight;
+ // 返回坐标
+ return new Point(x, y);
+ }
+
+ ///
+ /// 更新悬停框
+ ///
+ public void UpdateHoverBox() => _hoverBoxLayer.Update();
+
+ ///
+ /// 清空选框
+ ///
+ public void ClearSelectBox() => _selectBoxLayer.Clear();
+
+ ///
+ /// 更新选框
+ ///
+ public void UpdateSelectBox(Point start, Point end)
+ {
+ _selectBoxLayer.Start = start;
+ _selectBoxLayer.End = end;
+ _selectBoxLayer.Update();
+ }
+
+ ///
+ /// 获取选框区域
+ ///
+ public Rect GetSelectBoxRect() => new Rect(_selectBoxLayer.Start, _selectBoxLayer.End);
+
+ ///
+ /// 获取选择方式
+ ///
+ public SelectType GetSelectType() =>
+ _selectBoxLayer.End.X < _selectBoxLayer.Start.X ? SelectType.Cross : SelectType.Box;
+
+ ///
+ /// 更新选中框
+ ///
+ public void UpdateSelectedBox()
+ {
+ _selectedBoxLayer.BoxList.Clear();
+ foreach (var card in GetComponent().SelectedCardList)
+ {
+ TargetBox box = new TargetBox
+ {
+ ScreenPoint = new Point(Canvas.GetLeft(card) + 9, Canvas.GetTop(card) - 2),
+ Width = card.Bounds.Width - 18,
+ Height = card.Bounds.Height + 4
+ };
+ _selectedBoxLayer.BoxList.Add(box);
+ }
+
+ _selectedBoxLayer.Update();
+ }
+
+ ///
+ /// 开始绘制临时连接线
+ ///
+ public void BeginDrawTempConnectLine(Point start)
+ {
+ _tempLineLayer.Line = new ConnectLine
+ {
+ Start = start,
+ End = start,
+ };
+ _tempLineLayer.Update();
+ }
+
+ ///
+ /// 更新临时连接线起点
+ ///
+ public void UpdateTempLineStart(Point start)
+ {
+ if (_tempLineLayer.Line != null)
+ _tempLineLayer.Line.Start = new Point(start.X, start.Y);
+ _tempLineLayer.Update();
+ }
+
+ ///
+ /// 设置临时连接线终点
+ ///
+ public void UpdateTempLineEnd(Point end)
+ {
+ if (_tempLineLayer.Line != null)
+ {
+ _tempLineLayer.Line.End = new Point(end.X, end.Y);
+ }
+
+ _tempLineLayer.Update();
+ }
+
+ ///
+ /// 清除临时连接线
+ ///
+ public void ClearTempLine()
+ {
+ _tempLineLayer.Line = null;
+ _tempLineLayer.Clear();
+ }
+
+ ///
+ /// 添加连接线
+ ///
+ public void AddConnectLine(PinBase start, PinBase end)
+ {
+ VisualConnectLine connectLine = new VisualConnectLine
+ {
+ StartPin = start,
+ EndPin = end,
+ Start = GetPinPoint(start),
+ End = GetPinPoint(end),
+ IsData = start is DataPin,
+ };
+ if (connectLine.IsData) connectLine.Color = GetPinColor((DataPin)start);
+ _connectLineLayer.AddConnectLine(connectLine);
+ connectLine.Update();
+ }
+
+ ///
+ /// 移除连接线
+ ///
+ public void RemoveConnectLine(PinBase start, PinBase end) => _connectLineLayer.RemoveConnectLine(start, end);
+
+ ///
+ /// 更新连接线
+ ///
+ public void UpdateConnectLine()
+ {
+ // 遍历连接线元素
+ foreach (var element in _connectLineLayer.ConnectLineList)
+ {
+ // 更新连接线的坐标
+ element.Start = GetPinPoint(element.StartPin);
+ element.End = GetPinPoint(element.EndPin);
+ // 重绘连接线
+ element.Update();
+ }
+ }
+
+ ///
+ /// 更新悬停连接线
+ ///
+ public void UpdateHoveredConnectLine(Point point)
+ {
+ _hoveredLine = _connectLineLayer.GetHitedVisualElement(point);
+ if (_hoveredLine is VisualConnectLine line)
+ {
+ _lineBackLayer.Start = GetPinPoint(line.StartPin);
+ _lineBackLayer.End = GetPinPoint(line.EndPin);
+ _lineBackLayer.Update();
+ }
+ else _lineBackLayer.Clear();
+ }
+
+ ///
+ /// 拖动视口
+ ///
+ public void DragViewport(Point offset)
+ {
+ // 移动网格
+ _gridLayer.MoveLayer(offset);
+ // 更新节点视图
+ GetComponent().UpdateNodeCard();
+ // 更新选中框与连接线
+ UpdateSelectedBox();
+ UpdateConnectLine();
+ }
+
+ ///
+ /// 结束拖动
+ ///
+ public void EndDrag() => _gridLayer.ApplyOffset();
+
+ #endregion
+
+ #region 控件事件
+
+ private void OperateArea_SizeChanged(object sender, SizeChangedEventArgs e)
+ {
+ UpdateLayerSize();
+ UpdateGrid();
+ GetComponent().UpdateNodeCard();
+ UpdateSelectedBox();
+ UpdateConnectLine();
+ }
+
+ #endregion
+
+ #region 私有方法
+
+ ///
+ /// 启用图层
+ ///
+ private void EnableLayer()
+ {
+ // 创建图层
+ _gridLayer = new GridLayer();
+ _lineBackLayer = new ConnectLineBackLayer();
+ _connectLineLayer = new ConnectLineLayer();
+ _hoverBoxLayer = new HoverBoxLayer();
+ _selectedBoxLayer = new SelectedBoxLayer();
+ _selectBoxLayer = new SelectBoxLayer();
+ _tempLineLayer = new TempConnectLineLayer();
+ // 添加图层
+ Host.Layer_Base.Children.Add(_gridLayer);
+ Host.Layer_Base.Children.Add(_lineBackLayer);
+ Host.Layer_Base.Children.Add(_connectLineLayer);
+ Host.Layer_Box.Children.Add(_hoverBoxLayer);
+ Host.Layer_Box.Children.Add(_selectedBoxLayer);
+ Host.Layer_Box.Children.Add(_selectBoxLayer);
+ Host.Layer_Temp.Children.Add(_tempLineLayer);
+ // 更新图层尺寸
+ UpdateLayerSize();
+ // 更新网格
+ UpdateGrid();
+ }
+
+ ///
+ /// 重置图层
+ ///
+ private void ResetLayer()
+ {
+ // 重置网格
+ _gridLayer.Reset();
+ _gridLayer.Update();
+ // 清空连接线
+ _lineBackLayer.Clear();
+ _connectLineLayer.ClearConnectLine();
+ // 清空悬停框、选框
+ _hoverBoxLayer.Box = null;
+ _hoverBoxLayer.Clear();
+ _selectBoxLayer.Clear();
+ // 清空选中框
+ _selectedBoxLayer.Clear();
+ _selectedBoxLayer.BoxList.Clear();
+ }
+
+ ///
+ /// 更新图层尺寸
+ ///
+ private void UpdateLayerSize()
+ {
+ double width = Host.OperateArea.Bounds.Width;
+ double height = Host.OperateArea.Bounds.Height;
+
+ _gridLayer.Width = width;
+ _gridLayer.Height = height;
+ _lineBackLayer.Width = width;
+ _lineBackLayer.Height = height;
+ _connectLineLayer.Width = width;
+ _connectLineLayer.Height = height;
+ _hoverBoxLayer.Width = width;
+ _hoverBoxLayer.Height = height;
+ _selectBoxLayer.Width = width;
+ _selectBoxLayer.Height = height;
+ _selectedBoxLayer.Width = width;
+ _selectedBoxLayer.Height = height;
+ _tempLineLayer.Width = width;
+ _tempLineLayer.Height = height;
+ }
+
+ ///
+ /// 更新网格
+ ///
+ private void UpdateGrid() => _gridLayer.Update();
+
+ ///
+ /// 获取引脚相对于图层的坐标
+ ///
+ private Point GetPinPoint(PinBase pin)
+ {
+ // 获取引脚路径
+ PinPath path = pin.GetPinPath();
+ // 获取节点卡片
+ NodeView card = GetComponent().GetNodeCard(path.NodeID);
+ // 获取引脚坐标(相对于节点)
+ Point pinOffset = card.GetPinOffset(path);
+ // 获取节点坐标
+ Point nodePoint = new Point(Canvas.GetLeft(card), Canvas.GetTop(card));
+ // 返回偏移
+ return new Point(nodePoint.X + pinOffset.X, nodePoint.Y + pinOffset.Y);
+ }
+
+ ///
+ /// 获取引脚颜色
+ ///
+ private Color GetPinColor(DataPin pin)
+ {
+ return ((DataPinGroup)pin.OwnerGroup).Type switch
+ {
+ "bool" => PinColorSet.Bool,
+ "int" => PinColorSet.Int,
+ "double" => PinColorSet.Double,
+ "string" => PinColorSet.String,
+ "byte[]" => PinColorSet.ByteArray,
+ _ => Colors.White,
+ };
+ }
+
+ #endregion
+
+ #region 字段
+
+ /// 网格图层
+ private GridLayer? _gridLayer;
+
+ /// 连接线背景图层
+ private ConnectLineBackLayer? _lineBackLayer;
+
+ /// 连接线图层
+ private ConnectLineLayer? _connectLineLayer;
+
+ /// 悬停框图层
+ private HoverBoxLayer? _hoverBoxLayer;
+
+ /// 选框图层
+ private SelectBoxLayer? _selectBoxLayer;
+
+ /// 选中框图层
+ private SelectedBoxLayer? _selectedBoxLayer;
+
+ /// 临时连接线图层
+ private TempConnectLineLayer? _tempLineLayer;
+
+ /// 悬停连接线
+ private VisualElement? _hoveredLine = null;
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Panel/Component/EditerComponent.cs b/AXNode/SubSystem/NodeEditSystem/Panel/Component/EditerComponent.cs
new file mode 100644
index 0000000..ca28115
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Panel/Component/EditerComponent.cs
@@ -0,0 +1,15 @@
+using XLib.Base.UIComponent;
+
+namespace AXNode.SubSystem.NodeEditSystem.Panel.Component
+{
+ public class EditerComponent : Component
+ {
+ #region 生命周期
+
+ #endregion
+
+ #region 公开方法
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Panel/Component/InteractionComponent.cs b/AXNode/SubSystem/NodeEditSystem/Panel/Component/InteractionComponent.cs
new file mode 100644
index 0000000..0d264cc
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Panel/Component/InteractionComponent.cs
@@ -0,0 +1,800 @@
+using System;
+using System.Collections.Generic;
+using System.Windows;
+using System.Windows.Input;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Layout;
+using XLib.Base;
+using XLib.Base.UIComponent;
+using XLib.Base.VirtualDisk;
+using XLib.Node;
+using AXNode.SubSystem.NodeEditSystem.Control;
+using AXNode.SubSystem.NodeEditSystem.Define;
+using AXNode.SubSystem.ProjectSystem;
+using AXNode.SubSystem.ResourceSystem;
+using AXNode.SubSystem.WindowSystem;
+using XLib.AvaloniaControl.Tool;
+using AXNode.SubSystem.NodeEditSystem.Control;
+using XLib.AvaloniaControl;
+
+namespace AXNode.SubSystem.NodeEditSystem.Panel.Component
+{
+ ///
+ /// 交互组件:处理键盘、鼠标事件
+ ///
+ public class InteractionComponent : Component
+ {
+ #region 属性
+
+ public NodeView? HoveredCard => _hoveredNodeView;
+
+ #endregion
+
+ #region 生命周期
+
+ protected override void Init()
+ {
+ _tool = new SelectTool(this);
+ _tool.Init();
+ _hoverToolBar = new HoverToolBar
+ {
+ HorizontalAlignment = HorizontalAlignment.Left,
+ VerticalAlignment = VerticalAlignment.Top,
+ };
+ _host.LayerBox_ToolBar.Children.Add(_hoverToolBar);
+ _hoverToolBar.UpdateLayout();
+ _hoverToolBar.IsVisible = false;
+ _hoverToolBar.Init();
+ _hoverToolBar.ToolClick += HoverToolBar_ToolClick;
+ }
+
+ protected override void Enable()
+ {
+ _host.OperateArea.PointerMoved += OperateArea_MouseMove;
+ _host.OperateArea.PointerPressed += OperateArea_MouseDown;
+ _host.OperateArea.PointerReleased += OperateArea_MouseUp;
+ }
+
+ protected override void Reset()
+ {
+ ResetComponent();
+ }
+
+ protected override void Disable()
+ {
+ ResetComponent();
+ _host.OperateArea.PointerMoved -= OperateArea_MouseMove;
+ _host.OperateArea.PointerPressed -= OperateArea_MouseDown;
+ _host.OperateArea.PointerReleased -= OperateArea_MouseUp;
+ _hoverToolBar.IsVisible = false;
+ }
+
+ protected override void Remove()
+ {
+ ResetComponent();
+ _host.OperateArea.PointerMoved -= OperateArea_MouseMove;
+ _host.OperateArea.PointerPressed -= OperateArea_MouseDown;
+ _host.OperateArea.PointerReleased -= OperateArea_MouseUp;
+ _hoverToolBar.ToolClick -= HoverToolBar_ToolClick;
+ _host.LayerBox_ToolBar.Children.Remove(_hoverToolBar);
+ _hoverToolBar = null;
+ }
+
+ #endregion
+
+ #region 公开方法
+
+ public async void HandleKeyDown(KeyEventArgs e)
+ {
+ if (e.Key == Key.Delete)
+ {
+ List cardList = GetComponent().SelectedCardList;
+ if (cardList.Count == 0) return;
+
+ bool? delete = await WM.ShowAsk("确定删除选中节点吗?", "确定", false, TipLevel.Warning);
+ if (delete != true) return;
+
+ DeleteNode(cardList);
+ }
+ else if (e.Key == Key.Space)
+ {
+ List cardList = GetComponent().SelectedCardList;
+ if (cardList.Count == 0) return;
+
+ foreach (NodeView card in cardList)
+ {
+ if (card.NodeInstance.State == NodeState.Disable) card.NodeInstance.Start();
+ else card.NodeInstance.Stop();
+ }
+ }
+ }
+
+ ///
+ /// 处理放下
+ ///
+ public void HandleDrop(List itemList)
+ {
+ // 获取屏幕坐标
+ var screenPoint = Mouse.GetPosition(_host.OperateArea, true);
+ // 获取节点组件
+ var component = GetComponent();
+ // 放下节点
+ bool added = false;
+ foreach (var item in itemList)
+ {
+ if (item is File file && file.Instance is NodeType nodeType)
+ {
+ // 放下并创建节点卡片
+ NodeView? nodeView = component.DropNode(file.ID, nodeType, screenPoint);
+ if (nodeView == null) continue;
+ nodeView.NodeBackMouseEnter = NodeBack_MouseEnter;
+ nodeView.NodeBackMouseLeave = NodeBack_MouseLeave;
+ nodeView.PinGroupListChanged = PinGroupListChanged;
+ nodeView.NodeChanged = NodeChanged;
+ added = true;
+ }
+ }
+
+ if (added) ProjectManager.Instance.Saved = false;
+ }
+
+ ///
+ /// 监听节点卡片
+ ///
+ public void ListenNodeCard(NodeView nodeView)
+ {
+ nodeView.NodeBackMouseEnter = NodeBack_MouseEnter;
+ nodeView.NodeBackMouseLeave = NodeBack_MouseLeave;
+ nodeView.PinGroupListChanged = PinGroupListChanged;
+ nodeView.NodeChanged = NodeChanged;
+ }
+
+ ///
+ /// 更新全部引脚图标
+ ///
+ public void UpdateAllPinIcon()
+ {
+ foreach (var card in GetComponent().AllCard) card.UpdateAllPinIcon();
+ }
+
+ #endregion
+
+ #region 工具方法
+
+ ///
+ /// 捕获操作图层
+ ///
+ public void CaptureOperationLayer() => _host.OperateArea.CaptureMouse();
+
+ ///
+ /// 释放操作图层
+ ///
+ public void ReleaseOperationLayer() => _host.OperateArea.ReleaseMouseCapture();
+
+ ///
+ /// 获取鼠标命中区域
+ ///
+ public MouseHitedArea GetHitedArea()
+ {
+ // 如果悬停节点不为空
+ if (_hoveredNodeView != null)
+ return _hoveredNodeView.HoveredPin == null ? MouseHitedArea.Node : MouseHitedArea.Pin;
+ // 如果悬停于连接线
+ else if (GetComponent().HoveredConnectLine != null) return MouseHitedArea.ConnectLine;
+ // 返回空白区域
+ return MouseHitedArea.Space;
+ }
+
+ ///
+ /// 清除悬停框
+ ///
+ public void ClearHoverBox()
+ {
+ GetComponent().HoverBox = null;
+ GetComponent().UpdateHoverBox();
+ }
+
+ ///
+ /// 处理鼠标移动
+ ///
+ public void HandleMouseMove()
+ {
+ // 重置光标
+ _tool.Cursor = CursorManager.Instance.Select;
+ // 悬停在引脚上:切换光标
+ if (_hoveredNodeView != null && _hoveredNodeView.HoveredPin != null)
+ _tool.Cursor = CursorManager.Instance.Cross;
+ // 设置光标
+ _host.OperateArea.Cursor = _tool.Cursor;
+
+ // 更新悬停连接线
+ GetComponent().UpdateHoveredConnectLine(Mouse.GetPosition(_host.OperateArea));
+ }
+
+ ///
+ /// 移除节点焦点
+ ///
+ public void RemoveNodeFocus() => _host.OperateArea.Focus();
+
+ ///
+ /// 启动并执行节点
+ ///
+ public void StartAndExecute()
+ {
+ _hoveredNodeView.NodeInstance.Start();
+ _hoveredNodeView.NodeInstance.Execute();
+ }
+
+ #region 选择
+
+ public bool CurrentNodeSelected() =>
+ GetComponent().SelectedCardList.Contains(_hoveredNodeView);
+
+ public void SetTop() => GetComponent().SetTop(_hoveredNodeView);
+
+ public void AddSelect()
+ {
+ GetComponent().AddSelect(_hoveredNodeView);
+ GetComponent().UpdateSelectedBox();
+ UpdateHoverToolBar();
+ UpdatePropertyPanel();
+ }
+
+ public void RemoveSelect()
+ {
+ GetComponent().RemoveSelect(_hoveredNodeView);
+ GetComponent().UpdateSelectedBox();
+ UpdateHoverToolBar();
+ UpdatePropertyPanel();
+ }
+
+ public void ClearSelect()
+ {
+ GetComponent().ClearSelect();
+ GetComponent().UpdateSelectedBox();
+ UpdateHoverToolBar();
+ UpdatePropertyPanel();
+ }
+
+ #endregion
+
+ #region 选框
+
+ public void BeginDrawSelectBox() => _mouseDown = Mouse.GetPosition(_host.OperateArea);
+
+ public void CancelDrawSelectBox() => GetComponent().ClearSelectBox();
+
+ public void DrawSelectBox() =>
+ GetComponent().UpdateSelectBox(_mouseDown, Mouse.GetPosition(_host.OperateArea));
+
+ public void EndDrawSelectBox()
+ {
+ // 清除选框
+ GetComponent().ClearSelectBox();
+ // 获取选框区域与选择方式
+ Rect rect = GetComponent().GetSelectBoxRect();
+ SelectType type = GetComponent().GetSelectType();
+ // 选择节点视图
+ foreach (var card in GetComponent().AllCard)
+ {
+ Rect nodeRect = card.GetHittableRect();
+ switch (type)
+ {
+ case SelectType.Box:
+ if (rect.Contains(nodeRect)) GetComponent().AddSelect(card);
+ break;
+ case SelectType.Cross:
+ if (rect.Intersects(nodeRect)) GetComponent().AddSelect(card);
+ break;
+ }
+ }
+
+ // 更新选中框
+ GetComponent().UpdateSelectedBox();
+ // 更新工具栏
+ UpdateHoverToolBar();
+ // 更新属性面板
+ UpdatePropertyPanel();
+ }
+
+ #endregion
+
+ #region 拖动节点
+
+ public void BeginDragNode()
+ {
+ _host.OperateArea.Cursor = CursorManager.Instance.SelectAndMove;
+ _mouseDown = Mouse.GetPosition(_host.OperateArea);
+ }
+
+ public void CancelDragNode()
+ {
+ _host.OperateArea.Cursor = _tool.Cursor;
+ }
+
+ public void DragNode()
+ {
+ // 更新当前坐标
+ _mousePoint = Mouse.GetPosition(_host.OperateArea);
+ // 计算偏移
+ Point offset = new Point(_mousePoint.X - _mouseDown.X, _mousePoint.Y - _mouseDown.Y);
+ // offset = new (0,0) ;
+
+ // 对齐网格
+ offset = new(Math.Round(offset.X / 10) * 10, offset.Y);
+ offset = new(offset.X, Math.Round(offset.Y / 10) * 10);
+
+ // 获取世界中心
+ Point center = GetComponent().WorldCenter;
+ // 设置节点偏移并更新坐标
+ foreach (var card in GetComponent().SelectedCardList)
+ {
+ card.SetOffset(new Point(offset.X, offset.Y));
+ Canvas.SetLeft(card, center.X + card.Point.X - 12);
+ Canvas.SetTop(card, center.Y + card.Point.Y - 1);
+ }
+
+ // 更新选中框、连接线
+ GetComponent().UpdateSelectedBox();
+ GetComponent().UpdateConnectLine();
+ // 更新悬浮工具栏
+ UpdateHoverToolBar();
+
+ ProjectManager.Instance.Saved = false;
+ }
+
+ public void EndDragNode()
+ {
+ _host.OperateArea.Cursor = _tool.Cursor;
+ foreach (var card in GetComponent().SelectedCardList) card.ApplyOffset();
+ }
+
+ #endregion
+
+ #region 连接线
+
+ public void BeginDrawConnectLine()
+ {
+ // 设置起始引脚
+ _startPin = _hoveredNodeView.HoveredPin;
+ // 设置鼠标坐标
+ _mouseDown = Mouse.GetPosition(_host.OperateArea);
+ // 获取引脚与鼠标的偏移量
+ Point offset = _hoveredNodeView.GetHoveredPinOffset();
+ // 计算引脚连接点坐标
+ Point pinPoint = new Point(_mouseDown.X + offset.X, _mouseDown.Y + offset.Y);
+ // 开始绘制连接线
+ GetComponent().BeginDrawTempConnectLine(pinPoint);
+ }
+
+ public void CancelDrawConnectLine()
+ {
+ GetComponent().ClearTempLine();
+ }
+
+ public void DrawConnectLine()
+ {
+ // 更新鼠标坐标
+ _mousePoint = Mouse.GetPosition(_host.OperateArea);
+ if (_hoveredNodeView != null && _hoveredNodeView.HoveredPin != null)
+ {
+ // 获取引脚与鼠标的偏移量
+ Point offset = _hoveredNodeView.GetHoveredPinOffset();
+ // 计算引脚连接点坐标
+ _mousePoint = new Point(_mousePoint.X + offset.X, _mousePoint.Y + offset.Y);
+ }
+
+ // 根据起始引脚类型更新连接线的起点或终点
+ if (_startPin.Flow == PinFlow.Input)
+ GetComponent().UpdateTempLineStart(_mousePoint);
+ else
+ GetComponent().UpdateTempLineEnd(_mousePoint);
+ }
+
+ public void EndDrawConnectLine()
+ {
+ // 清除临时连接线
+ GetComponent().ClearTempLine();
+ // 悬停引脚不为空
+ if (_hoveredNodeView != null && _hoveredNodeView.HoveredPin != null)
+ {
+ PinBase endPin = _hoveredNodeView.HoveredPin;
+ // 无法连接
+ if (!CanConnect(_startPin, endPin)) return;
+ // 连接引脚:将结束引脚写入起始引脚
+ if (_startPin.Flow == PinFlow.Input)
+ {
+ // 如果是数据引脚,先移除原有的连接线
+ if (_startPin is DataPin && _startPin.SourceList.Count > 0)
+ GetComponent().RemoveConnectLine(_startPin.SourceList[0], _startPin);
+ // 连接源与目标。数据引脚会自动断开原有连接
+ _startPin.AddSource(endPin);
+ endPin.AddTarget(_startPin);
+ // 添加连接线
+ GetComponent().AddConnectLine(endPin, _startPin);
+ }
+ else
+ {
+ // 如果是数据引脚,先移除原有的连接线
+ if (endPin is DataPin && endPin.SourceList.Count > 0)
+ GetComponent().RemoveConnectLine(endPin.SourceList[0], endPin);
+ // 连接源与目标。数据引脚会自动断开原有连接
+ _startPin.AddTarget(endPin);
+ endPin.AddSource(_startPin);
+ // 添加连接线
+ GetComponent().AddConnectLine(_startPin, endPin);
+ }
+
+ // 更新引脚图标
+ UpdateAllPinIcon();
+
+ ProjectManager.Instance.Saved = false;
+ }
+
+ _startPin = null;
+ }
+
+ #endregion
+
+ #region 断开引脚
+
+ public void BeginBreakPin() => _rightHitedPin = _hoveredNodeView.HoveredPin;
+
+ public void CancelBreakPin() => _rightHitedPin = null;
+
+ public void EndBreakPin()
+ {
+ // 命中输入节点,则与源断开
+ if (_rightHitedPin.Flow == PinFlow.Input)
+ {
+ List sourceList = new List(_rightHitedPin.SourceList);
+ foreach (var source in sourceList) BreakPin(source, _rightHitedPin);
+ }
+ // 否则与目标断开
+ else
+ {
+ List targetList = new List(_rightHitedPin.TargetList);
+ foreach (var target in targetList) BreakPin(_rightHitedPin, target);
+ }
+
+ // 更新引脚图标
+ UpdateAllPinIcon();
+
+ _rightHitedPin = null;
+
+ ProjectManager.Instance.Saved = false;
+ }
+
+ #endregion
+
+ #region 移除连接线
+
+ public void RemoveConnectLine()
+ {
+ // 获取连接线
+ Layer.VisualConnectLine? connectLine = GetComponent().HoveredConnectLine;
+ if (connectLine == null) return;
+ // 断开引脚
+ BreakPin(connectLine.StartPin, connectLine.EndPin);
+ // 更新引脚图标
+ UpdateAllPinIcon();
+ }
+
+ #endregion
+
+ #region 拖动视口
+
+ public void BeginDragViewport()
+ {
+ _host.OperateArea.Cursor = CursorManager.Instance.Move;
+ _mouseDown = Mouse.GetPosition(_host.OperateArea);
+ }
+
+ public void CancelDragViewport()
+ {
+ _host.OperateArea.Cursor = _tool.Cursor;
+ }
+
+ public void DragViewport()
+ {
+ _mousePoint = Mouse.GetPosition(_host.OperateArea);
+ GetComponent()
+ .DragViewport(new Point(_mousePoint.X - _mouseDown.X, _mousePoint.Y - _mouseDown.Y));
+ UpdateHoverToolBar();
+ }
+
+ public void EndDragViewport()
+ {
+ _host.OperateArea.Cursor = _tool.Cursor;
+ GetComponent().EndDrag();
+ }
+
+ #endregion
+
+ #endregion
+
+ #region 节点事件
+
+ private void NodeBack_MouseEnter(NodeView nodeView) => SwitchHoverTarget(nodeView);
+
+ private void NodeBack_MouseLeave(NodeView nodeView) => SwitchHoverTarget(null);
+
+ private void PinGroupListChanged()
+ {
+ // 更新选中框
+ GetComponent().UpdateSelectedBox();
+ // 更新连接线
+ GetComponent().UpdateConnectLine();
+ // 更新引脚图标
+ UpdateAllPinIcon();
+ }
+
+ private void NodeChanged()
+ {
+ ProjectManager.Instance.Saved = false;
+ }
+
+ #endregion
+
+ #region 控件事件
+
+ private void OperateArea_MouseMove(object? sender, PointerEventArgs pointerEventArgs) => _tool?.OnMouseMove();
+
+ private void OperateArea_MouseDown(object? sender, PointerPressedEventArgs e)
+ {
+ var point = e.GetCurrentPoint(sender as Avalonia.Controls.Control);
+ if (e.ClickCount == 1) _tool?.OnMouseDown(ToMouseBtn(point));
+ else if (point.Properties.IsLeftButtonPressed) _tool?.OnDoubleClick();
+ }
+
+ private void OperateArea_MouseUp(object? sender, PointerReleasedEventArgs e)
+ {
+ var point = e.GetCurrentPoint(sender as Avalonia.Controls.Control);
+
+ _tool?.OnMouseUp(ToMouseBtn(point));
+ }
+
+ private MouseButton ToMouseBtn(PointerPoint point)
+ {
+ return point.Properties.PointerUpdateKind switch
+ {
+ PointerUpdateKind.LeftButtonPressed => MouseButton.Left,
+ PointerUpdateKind.LeftButtonReleased => MouseButton.Left,
+ PointerUpdateKind.MiddleButtonPressed => MouseButton.Middle,
+ PointerUpdateKind.MiddleButtonReleased => MouseButton.Middle,
+ PointerUpdateKind.RightButtonPressed => MouseButton.Right,
+ PointerUpdateKind.RightButtonReleased => MouseButton.Right,
+ _ => MouseButton.None
+ };
+ }
+
+ private void HoverToolBar_ToolClick(string name)
+ {
+ switch (name)
+ {
+ case "Tool_Start":
+ foreach (var card in GetComponent().SelectedCardList)
+ card.NodeInstance.Start();
+ break;
+ case "Tool_Stop":
+ foreach (var card in GetComponent().SelectedCardList)
+ card.NodeInstance.Stop();
+ break;
+ case "Tool_Delete":
+ DeleteNode(GetComponent().SelectedCardList);
+ break;
+ }
+ }
+
+ #endregion
+
+ #region 私有方法
+
+ ///
+ /// 切换悬停目标
+ ///
+ private void SwitchHoverTarget(NodeView? target)
+ {
+ // 与当前目标一致,忽略
+ if (_hoveredNodeView == target) return;
+ // 更新目标
+ _hoveredNodeView = target;
+ // 当前目标为空,清空悬停框
+ if (_hoveredNodeView == null)
+ {
+ GetComponent().HoverBox = null;
+ GetComponent().UpdateHoverBox();
+ return;
+ }
+
+ // 当前目标已选中,不绘制悬停框
+ if (GetComponent().SelectedCardList.Contains(_hoveredNodeView)) return;
+
+ // 设置悬停框
+ GetComponent().HoverBox = new TargetBox
+ {
+ ScreenPoint = new Point(Canvas.GetLeft(_hoveredNodeView) + 9, Canvas.GetTop(_hoveredNodeView) - 2),
+ Width = _hoveredNodeView.Bounds.Width - 18,
+ Height = _hoveredNodeView.Bounds.Height + 4
+ };
+ }
+
+ ///
+ /// 更新悬停工具栏
+ ///
+ private void UpdateHoverToolBar()
+ {
+ // 选中数量
+ int selectedCount = GetComponent().SelectedCardList.Count;
+ // 显隐工具栏
+ _hoverToolBar.IsVisible = selectedCount == 0 ? false : true;
+
+ // 无选中
+ if (selectedCount == 0)
+ {
+ }
+ // 选中一个
+ else if (selectedCount == 1)
+ {
+ Rect cardRect = GetComponent().SelectedCardList[0].GetHittableRect();
+ double left = cardRect.Right - _hoverToolBar.Bounds.Width;
+ double top = cardRect.Top - 10 - _hoverToolBar.Bounds.Height;
+ Canvas.SetLeft(_hoverToolBar, left + 1);
+ Canvas.SetTop(_hoverToolBar, top + 1);
+ }
+ // 选中多个
+ else
+ {
+ List selectedList = GetComponent().SelectedCardList;
+ Rect rect = selectedList[0].GetHittableRect();
+ for (int index = 1; index < selectedList.Count; index++)
+ {
+ rect = rect.Union(selectedList[index].GetHittableRect());
+ }
+
+ double left = Math.Round((rect.Right - rect.Left - _hoverToolBar.Bounds.Width) / 2) + rect.Left;
+ double top = rect.Top - 10 - _hoverToolBar.Bounds.Height;
+ Canvas.SetLeft(_hoverToolBar, left + 1);
+ Canvas.SetTop(_hoverToolBar, top + 1);
+ }
+ }
+
+ ///
+ /// 更新属性面板
+ ///
+ private void UpdatePropertyPanel()
+ {
+ // 选中数量
+ int selectedCount = GetComponent().SelectedCardList.Count;
+ // 显隐属性面板
+ _host.PropertyArea.IsVisible = selectedCount == 0 ? false : true;
+
+ if (selectedCount == 0)
+ {
+ _host.PropertyPanel.Instance = null;
+ _host.PropertyPanel.ClearPropertyBar();
+ }
+ else
+ {
+ // 获取第一个选中的节点
+ NodeView firstCard = GetComponent().SelectedCardList[0];
+ if (firstCard.NodeInstance.PropertyList.Count == 0)
+ {
+ _host.PropertyArea.IsVisible = false;
+ return;
+ }
+
+ // 加载属性条
+ _host.PropertyPanel.Instance = firstCard.NodeInstance;
+ _host.PropertyPanel.LoadPropertyBar();
+ }
+ }
+
+ ///
+ /// 判断两个引脚能否连接
+ ///
+ private bool CanConnect(PinBase start, PinBase end)
+ {
+ // 不能连接自己
+ if (end == start) return false;
+ // 不能处于同一节点下
+ if (end.OwnerGroup.OwnerNode == start.OwnerGroup.OwnerNode) return false;
+ // 流向不能一致
+ if (end.Flow == start.Flow) return false;
+ // 类型必须一致
+ if (end.GetType() != start.GetType()) return false;
+ // 已连接
+ if (start.TargetList.Contains(end)) return false;
+
+ return true;
+ }
+
+ ///
+ /// 断开引脚
+ ///
+ private void BreakPin(PinBase source, PinBase target)
+ {
+ source.TargetList.Remove(target);
+ target.SourceList.Remove(source);
+ GetComponent().RemoveConnectLine(source, target);
+ }
+
+ ///
+ /// 删除节点
+ ///
+ private void DeleteNode(List cardList)
+ {
+ // 删除节点实例与卡片
+ foreach (var card in cardList)
+ {
+ GetComponent().DeleteNode(card.NodeInstance);
+ GetComponent().DeleteNodeCard(card);
+ }
+
+ // 更新引脚图标
+ UpdateAllPinIcon();
+ // 清空选择
+ GetComponent().ClearSelect();
+ // 更新选中框
+ GetComponent().UpdateSelectedBox();
+ // 更新悬浮工具栏
+ UpdateHoverToolBar();
+ // 更新属性面板
+ UpdatePropertyPanel();
+ // 更新鼠标悬停
+ HandleMouseMove();
+
+ ProjectManager.Instance.Saved = false;
+ }
+
+ ///
+ /// 重置组件
+ ///
+ private void ResetComponent()
+ {
+ ReleaseOperationLayer();
+ Host.Cursor = null;
+ _tool.Reset();
+ _hoveredNodeView = null;
+
+ _mouseDown = new Point();
+ _mousePoint = new Point();
+
+ _startPin = null;
+ _rightHitedPin = null;
+
+ _hoverToolBar.IsVisible = false;
+ _host.PropertyPanel.Instance = null;
+ _host.PropertyPanel.ClearPropertyBar();
+ _host.PropertyArea.IsVisible = false;
+ }
+
+ #endregion
+
+ #region 字段
+
+ private SelectTool _tool;
+
+ /// 当前悬停的节点视图
+ private NodeView? _hoveredNodeView = null;
+
+ /// 当前鼠标坐标
+ private Point _mousePoint = new Point();
+
+ /// 鼠标按下坐标
+ private Point _mouseDown = new Point();
+
+ /// 起始引脚
+ private PinBase? _startPin;
+
+ /// 右键命中引脚
+ private PinBase? _rightHitedPin = null;
+
+ /// 悬浮工具栏
+ private HoverToolBar _hoverToolBar;
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Panel/Component/NodeComponent.cs b/AXNode/SubSystem/NodeEditSystem/Panel/Component/NodeComponent.cs
new file mode 100644
index 0000000..4f6dc34
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Panel/Component/NodeComponent.cs
@@ -0,0 +1,130 @@
+using System.Collections.Generic;
+using System.Windows;
+using Avalonia;
+using AXNode.SubSystem.NodeEditSystem.Control;
+using XLib.Base.ID;
+using XLib.Base.UIComponent;
+using XLib.Node;
+using AXNode.SubSystem.NodeEditSystem.Control;
+
+namespace AXNode.SubSystem.NodeEditSystem.Panel.Component
+{
+ ///
+ /// 节点组件:管理编辑器中的节点实例
+ ///
+ public class NodeComponent : Component
+ {
+ #region 属性
+
+ /// 节点列表
+ public List NodeList => _nodeList;
+
+ #endregion
+
+ #region 生命周期
+
+ protected override void Reset()
+ {
+ _nodeIDBox.Reset();
+ _nodeDict.Clear();
+ _nodeList.Clear();
+ }
+
+ #endregion
+
+ #region 公开方法
+
+ ///
+ /// 放下节点
+ ///
+ public NodeView? DropNode(int fileID, NodeType nodeType, Point screenPoint)
+ {
+ // 获取世界坐标
+ var worldPoint = GetComponent().ScreenToWorld(screenPoint);
+ // 创建节点实例
+ NodeBase nodeInstance = nodeType.NewInstance();
+ nodeInstance.PinBreaked += Node_PinBreaked;
+ nodeInstance.TypeID = fileID;
+ // 设置节点编号、坐标
+ nodeInstance.ID = _nodeIDBox.TakeID();
+ nodeInstance.Point = new NodePoint((int)worldPoint.X, (int)worldPoint.Y);
+ // 添加节点引用
+ _nodeDict.Add(nodeInstance.ID, nodeInstance);
+ _nodeList.Add(nodeInstance);
+ // 生成节点卡片
+ NodeView card = GetComponent().GenerateNodeCard(nodeInstance);
+ // 启动节点
+ nodeInstance.Start();
+ return card;
+ }
+
+ ///
+ /// 加载节点
+ ///
+ public void LoadNode(NodeBase node)
+ {
+ node.PinBreaked += Node_PinBreaked;
+ _nodeIDBox.UseID(node.ID);
+ _nodeDict.Add(node.ID, node);
+ _nodeList.Add(node);
+ NodeView card = GetComponent().GenerateNodeCard(node);
+ card.UpdateLayout();
+ GetComponent().ListenNodeCard(card);
+ }
+
+ ///
+ /// 删除节点
+ ///
+ public void DeleteNode(NodeBase node)
+ {
+ node.Clear();
+ node.BreakAllPin();
+ node.PinBreaked -= Node_PinBreaked;
+ _nodeIDBox.RecycleID(node.ID);
+ _nodeDict.Remove(node.ID);
+ _nodeList.Remove(node);
+ }
+
+ ///
+ /// 生成连接线
+ ///
+ public void GenerateConnectLine()
+ {
+ // 遍历节点
+ foreach (var node in _nodeList)
+ {
+ // 遍历全部引脚
+ foreach (var pin in node.GetAllPin())
+ {
+ // 忽略输入引脚与空输出引脚
+ if (pin.Flow == PinFlow.Input || pin.TargetList.Count == 0) continue;
+ // 添加连接线
+ foreach (var target in pin.TargetList)
+ GetComponent().AddConnectLine(pin, target);
+ }
+ }
+ }
+
+ #endregion
+
+ #region 节点事件
+
+ private void Node_PinBreaked(PinBase start, PinBase end) =>
+ GetComponent().RemoveConnectLine(start, end);
+
+ #endregion
+
+ #region 字段
+
+ /// 节点编号箱
+ private readonly IDBox _nodeIDBox = new IDBox();
+
+ /// 节点字典
+ private readonly Dictionary _nodeDict = new Dictionary();
+
+ /// 节点列表
+ private List _nodeList = new List();
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Panel/EditPanel.axaml b/AXNode/SubSystem/NodeEditSystem/Panel/EditPanel.axaml
new file mode 100644
index 0000000..db99960
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Panel/EditPanel.axaml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Panel/EditPanel.axaml.cs b/AXNode/SubSystem/NodeEditSystem/Panel/EditPanel.axaml.cs
new file mode 100644
index 0000000..e95d3a4
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Panel/EditPanel.axaml.cs
@@ -0,0 +1,159 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Avalonia.Controls;
+using Avalonia.Input;
+using XLib.Base;
+using XLib.Base.UIComponent;
+using XLib.Base.VirtualDisk;
+using XLib.Node;
+using AXNode.SubSystem.EventSystem;
+using AXNode.SubSystem.NodeEditSystem.Define;
+using AXNode.SubSystem.NodeEditSystem.Panel.Component;
+using AXNode.SubSystem.NodeEditSystem.Define;
+using AXNode.SubSystem.NodeEditSystem.Panel.Component;
+using XLib.AvaloniaControl;
+
+namespace AXNode.SubSystem.NodeEditSystem.Panel;
+
+public partial class EditPanel : UserControl, IDropable
+{
+ #region 属性
+
+ /// 节点列表
+ public List NodeList => _nodeComponent.NodeList;
+
+ #endregion
+
+ #region 构造方法
+
+ public EditPanel()
+ {
+ InitializeComponent();
+ // Mouse.InitDropable(this);
+
+ DragDrop.SetAllowDrop(this, true);
+ this.AddHandler(DragDrop.DropEvent, ((sender, args) =>
+ {
+ Mouse.RegisterDragEventArgs(args);
+ if (args.Data.Get("object") is ITreeItem treeItem)
+ {
+ if (this is IDropable dropable)
+ {
+ try
+ {
+ dropable.OnDrop(new List { treeItem });
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine("拖动失败:" + ex.Message + ex.StackTrace);
+ }
+ }
+ }
+ }));
+ }
+
+ #endregion
+
+ #region 生命周期
+
+ public void Init()
+ {
+ // 添加核心组件
+ _editerComponent = _componentBox.AddComponent(this, "编辑器组件");
+ // 添加功能组件
+ _drawingComponent = _componentBox.AddComponent(this, "绘图组件");
+ _nodeComponent = _componentBox.AddComponent(this, "节点组件");
+ _cardComponent = _componentBox.AddComponent(this, "卡片组件");
+ _interactionComponent = _componentBox.AddComponent(this, "交互组件");
+ // 注册核心组件
+ _componentBox.RegisterCoreComponent(_editerComponent);
+ // 注册功能组件
+ _editerComponent.AddComponent(_drawingComponent);
+ _editerComponent.AddComponent(_nodeComponent);
+ _editerComponent.AddComponent(_cardComponent);
+ _editerComponent.AddComponent(_interactionComponent);
+ // 初始化组件
+ _componentBox.Init();
+ // 启用编辑
+ _editerComponent.ReqEnable();
+ // 监听系统事件
+ EM.Instance.Add(EventType.Project_Loaded, Project_Loaded);
+ }
+
+ #endregion
+
+ #region IDropable 方法
+
+ public void OnDrag(List fileList)
+ {
+ }
+
+ public void OnDrop(List fileList)
+ {
+ _interactionComponent.HandleDrop(fileList);
+ }
+
+ public bool CanDrop(List fileList) => fileList[0] is File file && file.Extension == "nt";
+
+ #endregion
+
+ #region 公开方法
+
+ ///
+ /// 重置
+ ///
+ public void Reset() => _componentBox.Reset();
+
+ ///
+ /// 加载节点
+ ///
+ public void LoadNode(NodeBase node) => _nodeComponent.LoadNode(node);
+
+ ///
+ /// 查找引脚
+ ///
+ public PinBase? FindPin(PinPath path)
+ {
+ foreach (var node in _nodeComponent.NodeList)
+ if (node.ID == path.NodeID)
+ return node.FindPin(path.NodeVersion, path.GroupIndex, path.PinIndex);
+ return null;
+ }
+
+ #endregion
+
+ #region 系统事件
+
+ private void Project_Loaded()
+ {
+ // 更新引脚图标
+ _interactionComponent.UpdateAllPinIcon();
+ // 生成连接线
+ _nodeComponent.GenerateConnectLine();
+ }
+
+ #endregion
+
+ #region 字段
+
+ /// 组件箱
+ private readonly ComponentBox _componentBox = new ComponentBox();
+
+ /// 编辑器组件
+ private EditerComponent _editerComponent;
+
+ /// 绘图组件
+ private DrawingComponent _drawingComponent;
+
+ /// 节点组件
+ private NodeComponent _nodeComponent;
+
+ /// 卡片组件
+ private CardComponent _cardComponent;
+
+ /// 交互组件
+ private InteractionComponent _interactionComponent;
+
+ #endregion
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Panel/Layer/ConnectLineBackLayer.cs b/AXNode/SubSystem/NodeEditSystem/Panel/Layer/ConnectLineBackLayer.cs
new file mode 100644
index 0000000..d3b4bf7
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Panel/Layer/ConnectLineBackLayer.cs
@@ -0,0 +1,59 @@
+using System.Windows;
+using Avalonia;
+using Avalonia.Media;
+using XLib.Avalonia.Drawing;
+
+namespace AXNode.SubSystem.NodeEditSystem.Panel.Layer
+{
+ ///
+ /// 连接线背景图层
+ ///
+ public class ConnectLineBackLayer : SingleBoard
+ {
+ public Point Start { get; set; }
+ public Point End { get; set; }
+
+
+ public override void Render(DrawingContext context)
+ {
+ // 计算连接线区域
+ _left = Start.X;
+ _right = End.X;
+ _top = Start.Y + 0.5;
+ _bottom = End.Y + 0.5;
+
+ // 计算贝塞尔曲线的控制线长度
+ double controlLineLength = (_right - _left) / 2;
+ if (controlLineLength < _minLength) controlLineLength = _minLength;
+
+ // 创建形状
+ PathGeometry geometry = new PathGeometry();
+ PathFigure figure = new PathFigure();
+ figure.IsClosed = false;
+ geometry.Figures.Add(figure);
+
+ // 计算贝塞尔曲线的控制点与终点
+ Point p1 = new Point(_left + controlLineLength, _top);
+ Point p2 = new Point(_right - controlLineLength, _bottom);
+ Point endPoint = new Point(_right, _bottom);
+
+ // 设置起点并添加贝塞尔曲线
+ figure.StartPoint = new Point(_left, _top);
+ var bs = new BezierSegment();
+ (bs.Point1, bs.Point2, bs.Point3, bs.IsStroked) = (p1, p2, endPoint, true);
+ figure.Segments.Add(bs);
+
+ context.DrawGeometry(null, _pen, geometry);
+ }
+
+ private double _left = 0;
+ private double _right = 0;
+ private double _top = 0;
+ private double _bottom = 0;
+
+ /// 控制线最短长度
+ private const int _minLength = 40;
+
+ private readonly Pen _pen = new Pen(new SolidColorBrush(Color.FromArgb(64, 255, 255, 255)), 5);
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Panel/Layer/ConnectLineLayer.cs b/AXNode/SubSystem/NodeEditSystem/Panel/Layer/ConnectLineLayer.cs
new file mode 100644
index 0000000..45057c1
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Panel/Layer/ConnectLineLayer.cs
@@ -0,0 +1,55 @@
+using System.Collections.Generic;
+using XLib.Node;
+using XLib.Avalonia.Drawing;
+
+namespace AXNode.SubSystem.NodeEditSystem.Panel.Layer
+{
+ ///
+ /// 连接线图层
+ ///
+ public class ConnectLineLayer : DrawingBoard
+ {
+ public List ConnectLineList => _connectLineList;
+
+ public void AddConnectLine(VisualConnectLine connectLine)
+ {
+ _connectLineList.Add(connectLine);
+ // 添加可视元素
+ AddVisualElement(connectLine);
+ // 绘制可视元素
+ connectLine.Update();
+ }
+
+ public void RemoveConnectLine(PinBase start, PinBase end)
+ {
+ int lineIndex = -1;
+ int index = 0;
+ // 查找连接线
+ foreach (var line in _connectLineList)
+ {
+ if (line.StartPin == start && line.EndPin == end)
+ {
+ lineIndex = index;
+ break;
+ }
+
+ index++;
+ }
+
+ // 移除连接线
+ if (lineIndex != -1)
+ {
+ RemoveVisualElement(_connectLineList[lineIndex]);
+ _connectLineList.RemoveAt(lineIndex);
+ }
+ }
+
+ public void ClearConnectLine()
+ {
+ _connectLineList.Clear();
+ ClearVisualElement();
+ }
+
+ private readonly List _connectLineList = new List();
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Panel/Layer/GridLayer.cs b/AXNode/SubSystem/NodeEditSystem/Panel/Layer/GridLayer.cs
new file mode 100644
index 0000000..32a0205
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Panel/Layer/GridLayer.cs
@@ -0,0 +1,213 @@
+using System.Runtime.CompilerServices;
+using System.Windows;
+using Avalonia;
+using Avalonia.Media;
+using XLib.Avalonia.Drawing;
+
+namespace AXNode.SubSystem.NodeEditSystem.Panel.Layer
+{
+ ///
+ /// 网格图层
+ ///
+ public class GridLayer : SingleBoard
+ {
+ #region 属性
+
+ public Point GridCenter { get; set; }
+
+ public int GridLineCount { get; private set; }
+
+ public int GridListCount { get; private set; }
+
+ public int CellWidth => _gridWidth / _subdivideWidth;
+
+ public int CellHeight => _gridHeight / _subdivideHeight;
+
+ #endregion
+
+ #region 公开方法
+
+ public void MoveLayer(Point offset)
+ {
+ _moveOffset = offset;
+ Update();
+ }
+
+ public void Reset()
+ {
+ _centerOffset = new Point();
+ Update();
+ }
+
+ public override void Render(DrawingContext context)
+ {
+ DrawGrid(context);
+ }
+
+ ///
+ /// 结束拖动时调用
+ ///
+ public void ApplyOffset()
+ {
+ _centerOffset = new Point(_centerOffset.X + _moveOffset.X, _centerOffset.Y + _moveOffset.Y);
+ _moveOffset = new Point();
+ }
+
+ ///
+ /// 获取世界坐标
+ ///
+ public Point GetWorldPoint(Point screenPoint)
+ {
+ return new Point(screenPoint.X - GridCenter.X, screenPoint.Y - GridCenter.Y);
+ }
+
+ ///
+ /// 获取屏幕坐标
+ ///
+ public Point GetScreenPoint(Point worldPoint)
+ {
+ return new Point(GridCenter.X + worldPoint.X, GridCenter.Y + worldPoint.Y);
+ }
+
+ #endregion
+
+ #region 私有方法
+
+ ///
+ /// 绘制网格
+ ///
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void DrawGrid(DrawingContext context)
+ {
+ int line;
+ int list;
+ int subIndex;
+
+ // 更新中心点
+ Point realOffset = new Point(_centerOffset.X + _moveOffset.X, _centerOffset.Y + _moveOffset.Y);
+ GridCenter = new Point((int)Bounds.Width / 2 + realOffset.X, (int)Bounds.Height / 2 + realOffset.Y);
+
+ // 更新绘图起始点
+ UpdateDrawStart();
+ // 更新网格线数量
+ UpdateGridLineCount();
+
+ // 绘制细分线
+ _currentPen = _microLine;
+ for (line = 0; line < GridLineCount; line++)
+ for (subIndex = 1; subIndex < _subdivideHeight; subIndex++)
+ DrawHorizontalLine(context, line * _gridHeight + _gridHeight / _subdivideHeight * subIndex);
+ for (list = 0; list < GridListCount; list++)
+ for (subIndex = 1; subIndex < _subdivideWidth; subIndex++)
+ DrawVerticalLine(context, list * _gridWidth + _gridWidth / _subdivideWidth * subIndex);
+
+ // 绘制网格线
+ _currentPen = _normalLine;
+ for (line = 0; line < GridLineCount; line++)
+ {
+ if (_drawStart.Y + line * _gridHeight == GridCenter.Y) continue;
+ DrawHorizontalLine(context, line * _gridHeight);
+ }
+
+ for (list = 0; list < GridListCount; list++)
+ {
+ if (_drawStart.X + list * _gridWidth == GridCenter.X) continue;
+ DrawVerticalLine(context, list * _gridWidth);
+ }
+
+ // 绘制中心线
+ _currentPen = _centerLine;
+ DrawHorizontalLine(context, (int)GridCenter.Y - (int)_drawStart.Y);
+ _currentPen = _centerList;
+ DrawVerticalLine(context, (int)GridCenter.X - (int)_drawStart.X);
+ }
+
+ ///
+ /// 更新绘图起始点,即左上角
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void UpdateDrawStart()
+ {
+ // 中心点横坐标 < 0
+ if (GridCenter.X < 0)
+ _drawStart = new Point(GridCenter.X + (int)(0 - GridCenter.X) / _gridWidth * _gridWidth,
+ _drawStart.Y);
+ else
+ _drawStart = new Point(GridCenter.X - ((int)GridCenter.X / _gridWidth + 1) * _gridWidth,
+ _drawStart.Y);
+
+ // 中心点纵坐标 < 0
+ if (GridCenter.Y < 0)
+ _drawStart = new Point(_drawStart.X,
+ GridCenter.Y + (int)(0 - GridCenter.Y) / _gridHeight * _gridHeight);
+ else
+ _drawStart = new Point(_drawStart.X,
+ GridCenter.Y - ((int)GridCenter.Y / _gridHeight + 1) * _gridHeight);
+ }
+
+ ///
+ /// 更新网格线数量
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void UpdateGridLineCount()
+ {
+ GridLineCount = (int)(Bounds.Height / _gridHeight) + 2;
+ GridListCount = (int)(Bounds.Width / _gridWidth) + 2;
+ }
+
+ ///
+ /// 绘制水平线
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void DrawHorizontalLine(DrawingContext context, int y)
+ {
+ int realy = (int)_drawStart.Y + y;
+ if (realy < 0 || realy > Bounds.Height) return;
+ context.DrawLine(_currentPen, new Point(0, realy), new Point(Bounds.Width, realy));
+ }
+
+ ///
+ /// 绘制垂直线
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void DrawVerticalLine(DrawingContext context, int x)
+ {
+ int realx = (int)_drawStart.X + x;
+ if (realx < 0 || realx > Bounds.Width) return;
+ context.DrawLine(_currentPen, new Point(realx, 0), new Point(realx, Bounds.Height));
+ }
+
+ #endregion
+
+ #region 画笔、画刷
+
+ private readonly Pen _normalLine = new(new SolidColorBrush(Color.FromArgb(255, 30, 30, 30)), 2);
+ private readonly Pen _microLine = new(new SolidColorBrush(Color.FromArgb(255, 20, 20, 20)), 2);
+ private readonly Pen _centerLine = new(new SolidColorBrush(Color.FromArgb(255, 60, 60, 60)), 2);
+ private readonly Pen _centerList = new(new SolidColorBrush(Color.FromArgb(255, 60, 60, 60)), 2);
+ private Pen? _currentPen;
+
+ #endregion
+
+ #region 字段
+
+ /// 格子宽度
+ private readonly int _gridWidth = 120;
+
+ /// 宽度细分量
+ private readonly int _subdivideWidth = 4;
+
+ /// 格子高度
+ private readonly int _gridHeight = 120;
+
+ /// 高度细分量
+ private readonly int _subdivideHeight = 4;
+
+ private Point _drawStart = new Point();
+ private Point _centerOffset = new Point();
+ private Point _moveOffset = new Point();
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Panel/Layer/HoverBoxLayer.cs b/AXNode/SubSystem/NodeEditSystem/Panel/Layer/HoverBoxLayer.cs
new file mode 100644
index 0000000..964c83a
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Panel/Layer/HoverBoxLayer.cs
@@ -0,0 +1,46 @@
+using System.Collections.Generic;
+using System.Windows;
+using Avalonia;
+using Avalonia.Media;
+using Avalonia.Threading;
+using AXNode.SubSystem.NodeEditSystem.Define;
+using XLib.Animate;
+using XLib.Avalonia.Drawing;
+
+namespace AXNode.SubSystem.NodeEditSystem.Panel.Layer
+{
+ ///
+ /// 悬停框图层
+ ///
+ public class HoverBoxLayer : SingleBoard, IMotion
+ {
+ /// 悬停框
+ public TargetBox? Box { get; set; } = null;
+
+ public override void Render(DrawingContext context)
+ {
+ if (Box == null) return;
+
+ List pointList = Box.GetPointList(15);
+ for (int index = 0; index < pointList.Count / 2; index++)
+ context.DrawLine(_pen, pointList[index * 2], pointList[index * 2 + 1]);
+ }
+
+ public double GetMotionProperty(string propertyName) => 0;
+
+ public void SetMotionProperty(string propertyName, double value)
+ {
+ if (Box == null) return;
+ switch (propertyName)
+ {
+ case "BoxMargin":
+ Box.BoxOffset = value;
+ break;
+ }
+
+ Dispatcher.UIThread.Invoke(Update);
+ }
+
+ private readonly Pen _pen = new Pen(Brushes.White, 1);
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Panel/Layer/SelectBoxLayer.cs b/AXNode/SubSystem/NodeEditSystem/Panel/Layer/SelectBoxLayer.cs
new file mode 100644
index 0000000..c6bd057
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Panel/Layer/SelectBoxLayer.cs
@@ -0,0 +1,32 @@
+using System.Windows;
+using Avalonia;
+using Avalonia.Media;
+using XLib.Avalonia.Drawing;
+
+namespace AXNode.SubSystem.NodeEditSystem.Panel.Layer
+{
+ ///
+ /// 选框图层
+ ///
+ public class SelectBoxLayer : SingleBoard
+ {
+ public Point Start { get; set; } = new Point();
+
+ public Point End { get; set; } = new Point();
+
+ public override void Render(DrawingContext context)
+ {
+ base.Render(context);
+ var start = new Point(Start.X + 0.5, Start.Y + 0.5);
+ var end = new Point(End.X + 0.5, End.Y + 0.5);
+ if (End.X >= Start.X)
+ context.DrawRectangle(_blue, _border, new Rect(start, end));
+ else
+ context.DrawRectangle(_orange, _border, new Rect(start, end));
+ }
+
+ private readonly Pen _border = new Pen(new SolidColorBrush(Color.FromRgb(255, 255, 255)), 1);
+ private readonly Brush _blue = new SolidColorBrush(Color.FromArgb(32, 0, 128, 235));
+ private readonly Brush _orange = new SolidColorBrush(Color.FromArgb(32, 255, 120, 0));
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Panel/Layer/SelectedBoxLayer.cs b/AXNode/SubSystem/NodeEditSystem/Panel/Layer/SelectedBoxLayer.cs
new file mode 100644
index 0000000..7410990
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Panel/Layer/SelectedBoxLayer.cs
@@ -0,0 +1,29 @@
+using System.Collections.Generic;
+using System.Windows;
+using Avalonia;
+using Avalonia.Media;
+using AXNode.SubSystem.NodeEditSystem.Define;
+using XLib.Avalonia.Drawing;
+
+namespace AXNode.SubSystem.NodeEditSystem.Panel.Layer
+{
+ ///
+ /// 选中框图层
+ ///
+ public class SelectedBoxLayer : SingleBoard
+ {
+ public List BoxList { get; set; } = new List();
+
+ public override void Render(DrawingContext context)
+ {
+ foreach (var box in BoxList)
+ {
+ List pointList = box.GetPointList(15);
+ for (int index = 0; index < pointList.Count / 2; index++)
+ context.DrawLine(_pen, pointList[index * 2], pointList[index * 2 + 1]);
+ }
+ }
+
+ private readonly Pen _pen = new Pen(new SolidColorBrush(Color.FromRgb(237, 100, 21)), 1);
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Panel/Layer/TempConnectLineLayer.cs b/AXNode/SubSystem/NodeEditSystem/Panel/Layer/TempConnectLineLayer.cs
new file mode 100644
index 0000000..e567161
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Panel/Layer/TempConnectLineLayer.cs
@@ -0,0 +1,64 @@
+using System.Windows;
+using Avalonia;
+using Avalonia.Media;
+using AXNode.SubSystem.NodeEditSystem.Define;
+using XLib.Avalonia.Drawing;
+
+namespace AXNode.SubSystem.NodeEditSystem.Panel.Layer
+{
+ ///
+ /// 临时连接线图层
+ ///
+ public class TempConnectLineLayer : SingleBoard
+ {
+ public ConnectLine? Line { get; set; } = null;
+
+ public override void Render(DrawingContext context)
+ {
+ if (Line == null) return;
+
+ // 计算连接线区域
+ _left = Line.Start.X;
+ _right = Line.End.X;
+ _top = Line.Start.Y + 0.5;
+ _bottom = Line.End.Y + 0.5;
+ // 计算贝塞尔曲线的控制线长度
+ double controlLineLength = (_right - _left) / 2;
+ if (controlLineLength < _minLength) controlLineLength = _minLength;
+
+ // 创建形状
+ PathGeometry geometry = new PathGeometry();
+ PathFigure figure = new PathFigure();
+ figure.IsClosed = false;
+ geometry.Figures.Add(figure);
+
+ // 计算贝塞尔曲线的控制点与终点
+ Point p1 = new Point(_left + controlLineLength, _top);
+ Point p2 = new Point(_right - controlLineLength, _bottom);
+ Point endPoint = new Point(_right, _bottom);
+
+ // 设置起点并添加贝塞尔曲线
+ figure.StartPoint = new Point(_left, _top);
+ var bs = new BezierSegment();
+ (bs.Point1, bs.Point2, bs.Point3, bs.IsStroked) = (p1, p2, endPoint, true);
+ figure.Segments.Add(bs);
+
+ // 绘制形状
+ context.DrawGeometry(null, _pen, geometry);
+ }
+
+ #region 字段
+
+ private double _left = 0;
+ private double _right = 0;
+ private double _top = 0;
+ private double _bottom = 0;
+
+ /// 控制线最短长度
+ private readonly int _minLength = 40;
+
+ private readonly Pen _pen = new Pen(new SolidColorBrush(Color.FromArgb(255, 255, 255, 255)), 1);
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Panel/Layer/VisualConnectLine.cs b/AXNode/SubSystem/NodeEditSystem/Panel/Layer/VisualConnectLine.cs
new file mode 100644
index 0000000..57d4f72
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Panel/Layer/VisualConnectLine.cs
@@ -0,0 +1,79 @@
+using System.Windows;
+using Avalonia;
+using Avalonia.Media;
+using XLib.Node;
+using XLib.Avalonia.Drawing;
+
+namespace AXNode.SubSystem.NodeEditSystem.Panel.Layer
+{
+ public class VisualConnectLine : VisualElement
+ {
+ // public VisualConnectLine() => _penExecute.Freeze();
+
+ public override bool HitTest(Point point)
+ {
+ // todo
+ return false;
+ }
+
+ public PinBase StartPin { get; set; }
+
+ public PinBase EndPin { get; set; }
+
+ public Point Start { get; set; }
+
+ public Point End { get; set; }
+
+ public Color Color { get; set; } = Colors.White;
+
+ public bool IsData { get; set; } = false;
+
+ public override void Render(DrawingContext context)
+ {
+ // 计算连接线区域
+ _left = Start.X;
+ _right = End.X;
+ _top = Start.Y + 0.5;
+ _bottom = End.Y + 0.5;
+
+ // 计算贝塞尔曲线的控制线长度
+ double controlLineLength = (_right - _left) / 2;
+ if (controlLineLength < _minLength) controlLineLength = _minLength;
+
+ // 创建形状
+ PathGeometry geometry = new PathGeometry();
+ PathFigure figure = new PathFigure();
+ figure.IsClosed = false;
+ geometry.Figures.Add(figure);
+
+ // 计算贝塞尔曲线的控制点与终点
+ Point p1 = new Point(_left + controlLineLength, _top);
+ Point p2 = new Point(_right - controlLineLength, _bottom);
+ Point endPoint = new Point(_right, _bottom);
+
+ // 设置起点并添加贝塞尔曲线
+ figure.StartPoint = new Point(_left, _top);
+ var bs = new BezierSegment();
+ (bs.Point1, bs.Point2, bs.Point3, bs.IsStroked) = (p1, p2, endPoint, true);
+ figure.Segments.Add(bs);
+
+ // Pen pen = new Pen(new SolidColorBrush(Color), 1);
+ // 绘制形状
+ if (!IsData) context.DrawGeometry(null, _penExecute, geometry);
+ else context.DrawGeometry(null, new Pen(new SolidColorBrush(Color), 1), geometry);
+ }
+
+ public override string ToString() => $"{Start}-{End}";
+
+ private double _left = 0;
+ private double _right = 0;
+ private double _top = 0;
+ private double _bottom = 0;
+
+ /// 控制线最短长度
+ private readonly int _minLength = 40;
+
+ /// 执行引脚线
+ private readonly Pen _penExecute = new Pen(new SolidColorBrush(Color.FromArgb(255, 196, 126, 255)), 1);
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeEditSystem/Panel/SelectTool.cs b/AXNode/SubSystem/NodeEditSystem/Panel/SelectTool.cs
new file mode 100644
index 0000000..09d13ec
--- /dev/null
+++ b/AXNode/SubSystem/NodeEditSystem/Panel/SelectTool.cs
@@ -0,0 +1,253 @@
+using System;
+using System.Windows.Input;
+using Avalonia.Input;
+using AXNode;
+using AXNode.SubSystem.NodeEditSystem.Panel.Component;
+using XLib.Avalonia;
+using XLib.Avalonia.Behavior;
+using AXNode.SubSystem.NodeEditSystem.Panel.Component;
+using XLib.AvaloniaControl;
+
+namespace AXNode.SubSystem.NodeEditSystem.Panel
+{
+ public class SelectTool : ToolBase
+ {
+ public SelectTool(InteractionComponent host) : base(host)
+ {
+ }
+
+ /// 光标
+ public Cursor Cursor { get; set; }
+
+ public override void Init()
+ {
+ 移动();
+
+ 命中空白();
+ 命中节点();
+ 命中引脚();
+ 双击节点();
+
+ 中键按下();
+
+ 右键引脚();
+ 右键连接线();
+ }
+
+ public override void OnLeftButtonDown()
+ {
+ switch (_host.GetHitedArea())
+ {
+ case Define.MouseHitedArea.Space:
+ Invoke(Behaviors.HitedSpace);
+ break;
+ case Define.MouseHitedArea.Node:
+ Invoke("命中节点");
+ break;
+ case Define.MouseHitedArea.Pin:
+ Invoke("命中引脚");
+ break;
+ }
+ }
+
+ public override void OnDoubleClick()
+ {
+ if (_host.GetHitedArea() == Define.MouseHitedArea.Space)
+ Invoke("双击空白处");
+ else if (_host.GetHitedArea() == Define.MouseHitedArea.Node)
+ Invoke("双击节点");
+ }
+
+ public override void OnRightButtonDown()
+ {
+ var hitedArea = _host.GetHitedArea();
+ if (hitedArea == Define.MouseHitedArea.Pin)
+ Invoke("右键引脚");
+ else if (hitedArea == Define.MouseHitedArea.ConnectLine)
+ Invoke("右键连接线");
+ }
+
+ private void 移动()
+ {
+ NewTree(Behaviors.Move, (_) =>
+ {
+ _host.HandleMouseMove();
+ ResetTree();
+ });
+ Finish();
+ }
+
+ private void 命中空白()
+ {
+ NewTree(Behaviors.HitedSpace, (_) =>
+ {
+ _host.RemoveNodeFocus();
+ if (Keyboard.Modifiers != KeyModifiers.Control) _host.ClearSelect();
+ _host.BeginDrawSelectBox();
+ _host.CaptureOperationLayer();
+ });
+ NewNode(Behaviors.LeftUp, (_) =>
+ {
+ _host.CancelDrawSelectBox();
+ _host.ReleaseOperationLayer();
+ ResetTree();
+ });
+ BackToRoot();
+ NewNode(Behaviors.Move, (_) => { _host.DrawSelectBox(); });
+ NewNode(Behaviors.LeftUp, (_) =>
+ {
+ _host.EndDrawSelectBox();
+ _host.HandleMouseMove();
+ _host.ReleaseOperationLayer();
+ ResetTree();
+ });
+ Finish();
+ }
+
+ private void 命中节点()
+ {
+ NewTree("命中节点", (_) =>
+ {
+ // 清除悬停框
+ _host.ClearHoverBox();
+ // 按下了“Ctrl”键,加选或减选
+ if (Keyboard.Modifiers == KeyModifiers.Control)
+ {
+ if (_host.CurrentNodeSelected()) _host.RemoveSelect();
+ else _host.AddSelect();
+ }
+ // 当前节点未选中
+ else if (!_host.CurrentNodeSelected())
+ {
+ _host.ClearSelect();
+ _host.SetTop();
+ _host.AddSelect();
+ }
+
+ // 开始拖动节点
+ _host.BeginDragNode();
+ _host.CaptureOperationLayer();
+ });
+ NewNode(Behaviors.LeftUp, (_) =>
+ {
+ _host.CancelDragNode();
+ _host.HandleMouseMove();
+ _host.ReleaseOperationLayer();
+ ResetTree();
+ });
+ BackToRoot();
+ NewNode(Behaviors.Move, (_) => _host.DragNode());
+ NewNode(Behaviors.LeftUp, (_) =>
+ {
+ _host.EndDragNode();
+ _host.HandleMouseMove();
+ _host.ReleaseOperationLayer();
+ ResetTree();
+ });
+ Finish();
+ }
+
+ private void 命中引脚()
+ {
+ NewTree("命中引脚", (_) =>
+ {
+ _host.ClearHoverBox();
+ // 开始绘制连接线
+ _host.BeginDrawConnectLine();
+ });
+ // NewNode(Behaviors.LeftUp, (_) =>
+ // {
+ // // 取消绘制连接线
+ // _host.CancelDrawConnectLine();
+ // _host.HandleMouseMove();
+ // ResetTree();
+ // });
+ BackToRoot();
+ NewNode(Behaviors.Move, (_) =>
+ {
+ // 绘制连接线
+ _host.DrawConnectLine();
+ });
+ NewNode(Behaviors.LeftUp, (_) =>
+ {
+ // 结束绘制连接线
+ _host.EndDrawConnectLine();
+ _host.HandleMouseMove();
+ ResetTree();
+ });
+ Finish();
+ }
+
+ private void 双击节点()
+ {
+ NewTree("双击节点", (_) =>
+ {
+ _host.StartAndExecute();
+ ResetTree();
+ });
+ Finish();
+ }
+
+ private void 中键按下()
+ {
+ NewTree(Behaviors.MiddleDown, (_) =>
+ {
+ _host.ClearHoverBox();
+ _host.BeginDragViewport();
+ _host.CaptureOperationLayer();
+ });
+ NewNode(Behaviors.MiddleUp, (_) =>
+ {
+ _host.ReleaseOperationLayer();
+ _host.CancelDragViewport();
+ _host.HandleMouseMove();
+ ResetTree();
+ });
+ BackToRoot();
+ NewNode(Behaviors.Move, (_) => { _host.DragViewport(); });
+ NewNode(Behaviors.MiddleUp, (_) =>
+ {
+ _host.ReleaseOperationLayer();
+ _host.EndDragViewport();
+ _host.HandleMouseMove();
+ ResetTree();
+ });
+ Finish();
+ }
+
+ private void 右键引脚()
+ {
+ NewTree("右键引脚", (_) =>
+ {
+ _host.ClearHoverBox();
+ _host.BeginBreakPin();
+ });
+ NewNode(Behaviors.RightUp, (_) =>
+ {
+ _host.EndBreakPin();
+ _host.HandleMouseMove();
+ ResetTree();
+ });
+ BackToRoot();
+ NewNode(Behaviors.Move, (_) => { _host.CancelBreakPin(); });
+ NewNode(Behaviors.RightUp, (_) =>
+ {
+ _host.HandleMouseMove();
+ ResetTree();
+ });
+ Finish();
+ }
+
+ private void 右键连接线()
+ {
+ NewTree("右键连接线", null);
+ NewNode(Behaviors.RightUp, (_) =>
+ {
+ _host.RemoveConnectLine();
+ _host.HandleMouseMove();
+ ResetTree();
+ });
+ Finish();
+ }
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeLibSystem/Define/Data/Data_Double.cs b/AXNode/SubSystem/NodeLibSystem/Define/Data/Data_Double.cs
new file mode 100644
index 0000000..84cc463
--- /dev/null
+++ b/AXNode/SubSystem/NodeLibSystem/Define/Data/Data_Double.cs
@@ -0,0 +1,39 @@
+using System.Collections.Generic;
+using XLib.Node;
+
+namespace AXNode.SubSystem.NodeLibSystem.Define.Data
+{
+ public class Data_Double : NodeBase
+ {
+ public override void Init()
+ {
+ SetViewProperty(NodeColorSet.Data, "Variate", "小数");
+
+ PinGroupList.Add(new DataPinGroup(this, "double", "数据", "0.0")
+ {
+ Readable = true,
+ Writeable = false
+ });
+
+ InitPinGroup();
+ }
+
+ public override string GetTypeString() => nameof(Data_Double);
+
+ public override Dictionary GetParaDict()
+ {
+ Dictionary result = new Dictionary
+ {
+ { "Data", GetData(0) },
+ };
+ return result;
+ }
+
+ public override void LoadParaDict(string version, Dictionary paraDict)
+ {
+ SetData(0, paraDict["Data"]);
+ }
+
+ protected override NodeBase CloneNode() => new Data_Double();
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeLibSystem/Define/Data/Data_Int.cs b/AXNode/SubSystem/NodeLibSystem/Define/Data/Data_Int.cs
new file mode 100644
index 0000000..a8603c5
--- /dev/null
+++ b/AXNode/SubSystem/NodeLibSystem/Define/Data/Data_Int.cs
@@ -0,0 +1,39 @@
+using System.Collections.Generic;
+using XLib.Node;
+
+namespace AXNode.SubSystem.NodeLibSystem.Define.Data
+{
+ public class Data_Int : NodeBase
+ {
+ public override void Init()
+ {
+ SetViewProperty(NodeColorSet.Data, "Variate", "整数");
+
+ PinGroupList.Add(new DataPinGroup(this, "int", "数据", "0")
+ {
+ Readable = true,
+ Writeable = false
+ });
+
+ InitPinGroup();
+ }
+
+ public override string GetTypeString() => nameof(Data_Int);
+
+ public override Dictionary GetParaDict()
+ {
+ Dictionary result = new Dictionary
+ {
+ { "Data", GetData(0) },
+ };
+ return result;
+ }
+
+ public override void LoadParaDict(string version, Dictionary paraDict)
+ {
+ SetData(0, paraDict["Data"]);
+ }
+
+ protected override NodeBase CloneNode() => new Data_Int();
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeLibSystem/Define/Data/Data_String.cs b/AXNode/SubSystem/NodeLibSystem/Define/Data/Data_String.cs
new file mode 100644
index 0000000..5006b0c
--- /dev/null
+++ b/AXNode/SubSystem/NodeLibSystem/Define/Data/Data_String.cs
@@ -0,0 +1,40 @@
+using System.Collections.Generic;
+using XLib.Node;
+
+namespace AXNode.SubSystem.NodeLibSystem.Define.Data
+{
+ public class Data_String : NodeBase
+ {
+ public override void Init()
+ {
+ SetViewProperty(NodeColorSet.Data, "Variate", "字符串");
+
+ PinGroupList.Add(new DataPinGroup(this, "string", "数据", "")
+ {
+ BoxWidth = 240,
+ Readable = true,
+ Writeable = false
+ });
+
+ InitPinGroup();
+ }
+
+ public override string GetTypeString() => nameof(Data_String);
+
+ public override Dictionary GetParaDict()
+ {
+ Dictionary result = new Dictionary
+ {
+ { "Data", GetData(0) },
+ };
+ return result;
+ }
+
+ public override void LoadParaDict(string version, Dictionary paraDict)
+ {
+ SetData(0, paraDict["Data"]);
+ }
+
+ protected override NodeBase CloneNode() => new Data_String();
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeLibSystem/Define/Drivers/FrameDriver.cs b/AXNode/SubSystem/NodeLibSystem/Define/Drivers/FrameDriver.cs
new file mode 100644
index 0000000..a9a4b77
--- /dev/null
+++ b/AXNode/SubSystem/NodeLibSystem/Define/Drivers/FrameDriver.cs
@@ -0,0 +1,118 @@
+using System;
+using System.Collections.Generic;
+using AXNode.SubSystem.ControlSystem;
+using XLib.Base.Drive;
+using XLib.Node;
+
+namespace AXNode.SubSystem.NodeLibSystem.Define.Drivers
+{
+ ///
+ /// 帧驱动器
+ ///
+ public class FrameDriver : NodeBase, IFrameDriver
+ {
+ #region IFrameDriver 属性
+
+ public string Name { get; set; } = "帧驱动器";
+
+ #endregion
+
+ #region 生命周期
+
+ public override void Init()
+ {
+ SetViewProperty(NodeColorSet.Driver, "FrameDriver", "帧驱动器");
+
+ DataPinGroup fps = new DataPinGroup(this, "double", "帧率", "25") { Readable = false, Writeable = false };
+ fps.ValueChanged += FpsChanged;
+ PinGroupList.Add(fps);
+ PinGroupList.Add(new ActionPinGroup(this, "更新"));
+
+ InitPinGroup();
+ }
+
+ public override void Enable()
+ {
+ ControlEngine.Instance.Connect(this);
+ }
+
+ public override void Disable()
+ {
+ ControlEngine.Instance.Disconnect(this);
+ }
+
+ public override void Clear()
+ {
+ ControlEngine.Instance.Disconnect(this);
+ }
+
+ #endregion
+
+ #region 公开方法
+
+ public override string GetTypeString() => nameof(FrameDriver);
+
+ public override Dictionary GetParaDict() =>
+ new Dictionary { { "Fps", GetData(0) } };
+
+ public override void LoadParaDict(string version, Dictionary paraDict)
+ {
+ try
+ {
+ SetData(0, paraDict["Fps"]);
+ }
+ catch (Exception)
+ {
+ }
+ }
+
+ #endregion
+
+ #region IFrameDriver 方法
+
+ public void Update()
+ {
+ _delay++;
+ while (_delay >= _frameLength)
+ {
+ ((ActionPinGroup)PinGroupList[1]).Invoke();
+ _delay -= _frameLength;
+ }
+ }
+
+ #endregion
+
+ #region 内部方法
+
+ protected override NodeBase CloneNode() => new FrameDriver();
+
+ #endregion
+
+ #region 私有方法
+
+ private void FpsChanged()
+ {
+ try
+ {
+ double fps = double.Parse(GetData(0));
+ if (fps > 120) fps = 120;
+ if (fps > 0) _frameLength = 1000 / fps;
+ }
+ catch (Exception)
+ {
+ }
+ }
+
+ #endregion
+
+ #region 字段
+
+ /// 单帧时长:毫秒
+ private double _frameLength = 40;
+
+ /// 当前延迟
+ private double _delay = 0;
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeLibSystem/Define/Drivers/TimerDriver.cs b/AXNode/SubSystem/NodeLibSystem/Define/Drivers/TimerDriver.cs
new file mode 100644
index 0000000..92a9317
--- /dev/null
+++ b/AXNode/SubSystem/NodeLibSystem/Define/Drivers/TimerDriver.cs
@@ -0,0 +1,124 @@
+using System;
+using System.Collections.Generic;
+using AXNode.SubSystem.ControlSystem;
+using XLib.Base.Drive;
+using XLib.Base.Ex;
+using XLib.Node;
+
+namespace AXNode.SubSystem.NodeLibSystem.Define.Drivers
+{
+ ///
+ /// 定时驱动器
+ ///
+ public class TimerDriver : NodeBase, IFrameDriver, IProgressGetter
+ {
+ #region 生命周期
+
+ public override void Init()
+ {
+ SetViewProperty(NodeColorSet.Driver, "Timer", "定时驱动器");
+
+ DataPinGroup time = new DataPinGroup(this, "double", "间隔毫秒", "5000")
+ { Readable = false, Writeable = false };
+ time.ValueChanged += Time_ValueChanged;
+ PinGroupList.Add(time);
+ PinGroupList.Add(new ActionPinGroup(this, "更新"));
+
+ InitPinGroup();
+ }
+
+ public override void Enable()
+ {
+ OpenProgressBar?.Invoke(this);
+ ControlEngine.Instance.Connect(this);
+ }
+
+ public override void Disable()
+ {
+ CloseProgressBar?.Invoke();
+ ControlEngine.Instance.Disconnect(this);
+ _delay = 0;
+ }
+
+ public override void Clear()
+ {
+ CloseProgressBar?.Invoke();
+ ControlEngine.Instance.Disconnect(this);
+ _delay = 0;
+ }
+
+ #endregion
+
+ #region NodeBase 方法
+
+ public override string GetTypeString() => nameof(TimerDriver);
+
+ public override Dictionary GetParaDict()
+ {
+ return new Dictionary
+ {
+ { "Time", GetData(0) }
+ };
+ }
+
+ public override void LoadParaDict(string version, Dictionary paraDict)
+ {
+ try
+ {
+ SetData(0, paraDict["Time"]);
+ }
+ catch (Exception)
+ {
+ }
+ }
+
+ protected override NodeBase CloneNode() => new TimerDriver();
+
+ #endregion
+
+ #region IFrameDriver 方法
+
+ public void Update()
+ {
+ _delay++;
+ while (_delay >= _frameLength)
+ {
+ GetPinGroup(1).Invoke();
+ _delay -= _frameLength;
+ }
+ }
+
+ #endregion
+
+ #region IProgressGetter 方法
+
+ public double GetProgress() => _delay / _frameLength;
+
+ #endregion
+
+ #region 私有方法
+
+ private void Time_ValueChanged()
+ {
+ try
+ {
+ _frameLength = double.Parse(GetData(0)).Limit(1000 / 120.0, double.MaxValue);
+ }
+ catch (Exception)
+ {
+ }
+ }
+
+ #endregion
+
+ #region 字段
+
+ /// 单帧时长:毫秒
+ private double _frameLength = 5000;
+
+ /// 当前延迟
+ private double _delay = 0;
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeLibSystem/Define/Events/Event_Keyboard.cs b/AXNode/SubSystem/NodeLibSystem/Define/Events/Event_Keyboard.cs
new file mode 100644
index 0000000..c4fd9ab
--- /dev/null
+++ b/AXNode/SubSystem/NodeLibSystem/Define/Events/Event_Keyboard.cs
@@ -0,0 +1,76 @@
+using System;
+using System.Collections.Generic;
+using AXNode.SubSystem.EventSystem;
+using XLib.Node;
+
+namespace AXNode.SubSystem.NodeLibSystem.Define.Events
+{
+ ///
+ /// 键盘事件
+ ///
+ public class Event_Keyboard : NodeBase
+ {
+ public override void Init()
+ {
+ SetViewProperty(NodeColorSet.Event, "Key", "按键");
+
+ PinGroupList.Add(new DataPinGroup(this, "string", "当前按键", "") { BoxWidth = 120, Writeable = false });
+ PinGroupList.Add(new DataPinGroup(this, "string", "监听按键", "Space")
+ { BoxWidth = 120, Readable = false, Writeable = false });
+ PinGroupList.Add(new ActionPinGroup(this, "按下"));
+ PinGroupList.Add(new ActionPinGroup(this, "松开"));
+
+ InitPinGroup();
+ }
+
+ public override void Enable()
+ {
+ EM.Instance.Add(EventType.KeyDown, OnKeyDown);
+ EM.Instance.Add(EventType.KeyUp, OnKeyUp);
+ }
+
+ public override void Disable()
+ {
+ EM.Instance.Remove(EventType.KeyDown, OnKeyDown);
+ EM.Instance.Remove(EventType.KeyUp, OnKeyUp);
+ }
+
+ public override void Clear()
+ {
+ EM.Instance.Remove(EventType.KeyDown, OnKeyDown);
+ EM.Instance.Remove(EventType.KeyUp, OnKeyUp);
+ }
+
+ public override string GetTypeString() => nameof(Event_Keyboard);
+
+ public override Dictionary GetParaDict() =>
+ new Dictionary { { "ListenKey", GetData(1) } };
+
+ public override void LoadParaDict(string version, Dictionary paraDict)
+ {
+ try
+ {
+ SetData(1, paraDict["ListenKey"]);
+ }
+ catch (Exception)
+ {
+ }
+ }
+
+ protected override NodeBase CloneNode() => new Event_Keyboard();
+
+ private void OnKeyDown(string key)
+ {
+ SetData(0, key);
+ if (key == GetData(1))
+ GetPinGroup(2).Invoke();
+ }
+
+ private void OnKeyUp(string key)
+ {
+ SetData(0, key);
+ if (key == GetData(1))
+ GetPinGroup(3).Invoke();
+ }
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeLibSystem/Define/Flows/Flow_If.cs b/AXNode/SubSystem/NodeLibSystem/Define/Flows/Flow_If.cs
new file mode 100644
index 0000000..486af58
--- /dev/null
+++ b/AXNode/SubSystem/NodeLibSystem/Define/Flows/Flow_If.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Collections.Generic;
+using XLib.Node;
+
+namespace AXNode.SubSystem.NodeLibSystem.Define.Flows
+{
+ public class Flow_If : NodeBase
+ {
+ public override void Init()
+ {
+ SetViewProperty(NodeColorSet.Flow, "Flow", "判断");
+
+ PinGroupList.Add(new ExecutePinGroup(this, "根据逻辑值执行不同的逻辑"));
+ PinGroupList.Add(new DataPinGroup(this, "bool", "逻辑值", "True") { Readable = false });
+ PinGroupList.Add(new ActionPinGroup(this, "逻辑真"));
+ PinGroupList.Add(new ActionPinGroup(this, "逻辑假"));
+
+ InitPinGroup();
+ }
+
+ protected override void ExecuteNode()
+ {
+ // 获取逻辑值引脚组
+ DataPinGroup logicValueGroup = (DataPinGroup)PinGroupList[1];
+ // 执行连接至逻辑值的节点
+ if (logicValueGroup.InputPin != null && logicValueGroup.InputPin.SourceList.Count > 0)
+ // 输入引脚.第一个源引脚.所属引脚组.所属节点.执行
+ logicValueGroup.InputPin.SourceList[0].OwnerGroup.OwnerNode.Execute();
+ // 更新逻辑值
+ UpdateData(1);
+ // 解析值
+ bool logicResult = bool.Parse(GetData(1));
+
+ if (logicResult == true) GetPinGroup(2).Invoke();
+ else GetPinGroup(3).Invoke();
+
+ GetPinGroup().Execute();
+ }
+
+ public override string GetTypeString() => nameof(Flow_If);
+
+ public override Dictionary GetParaDict() =>
+ new Dictionary { { "LogicValue", GetData(1) } };
+
+ public override void LoadParaDict(string version, Dictionary paraDict)
+ {
+ try
+ {
+ SetData(1, paraDict["LogicValue"]);
+ }
+ catch (Exception)
+ {
+ }
+ }
+
+ protected override NodeBase CloneNode() => new Flow_If();
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeLibSystem/Define/Flows/Flow_LoopByCount.cs b/AXNode/SubSystem/NodeLibSystem/Define/Flows/Flow_LoopByCount.cs
new file mode 100644
index 0000000..b10f276
--- /dev/null
+++ b/AXNode/SubSystem/NodeLibSystem/Define/Flows/Flow_LoopByCount.cs
@@ -0,0 +1,34 @@
+using System.Collections.Generic;
+using XLib.Node;
+
+namespace AXNode.SubSystem.NodeLibSystem.Define.Flows
+{
+ public class Flow_LoopByCount : NodeBase
+ {
+ public override void Init()
+ {
+ SetViewProperty(NodeColorSet.Flow, "Loop", "计数循环");
+
+ PinGroupList.Add(new ExecutePinGroup(this, "循环执行多少次循环体"));
+ PinGroupList.Add(new DataPinGroup(this, "int", "次数", "10") { Readable = false });
+ PinGroupList.Add(new ActionPinGroup(this, "循环体"));
+
+ InitPinGroup();
+ }
+
+ protected override void ExecuteNode()
+ {
+ int count = int.Parse(GetData(1));
+ for (int counter = 0; counter < count; counter++)
+ GetPinGroup(2).Invoke();
+ GetPinGroup().Execute();
+ }
+
+ public override string GetTypeString() => nameof(Flow_LoopByCount);
+
+ public override Dictionary GetParaDict() =>
+ new Dictionary { { "Times", GetData(1) } };
+
+ protected override NodeBase CloneNode() => new Flow_LoopByCount();
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeLibSystem/Define/Flows/Flow_Switch.cs b/AXNode/SubSystem/NodeLibSystem/Define/Flows/Flow_Switch.cs
new file mode 100644
index 0000000..2079257
--- /dev/null
+++ b/AXNode/SubSystem/NodeLibSystem/Define/Flows/Flow_Switch.cs
@@ -0,0 +1,146 @@
+using System;
+using System.Collections.Generic;
+using XLib.Base.Ex;
+using XLib.Node;
+
+namespace AXNode.SubSystem.NodeLibSystem.Define.Flows
+{
+ public class Flow_Switch : NodeBase
+ {
+ public override void Init()
+ {
+ SetViewProperty(NodeColorSet.Flow, "Flow", "选择执行");
+
+ PinGroupList.Add(new ExecutePinGroup(this, "执行与值相等的引脚"));
+ PinGroupList.Add(new DataPinGroup(this, "string", "值", "") { BoxWidth = 120, Readable = false });
+ PinGroupList.Add(new ActionPinGroup(this, "Case_01"));
+ PinGroupList.Add(new ActionPinGroup(this, "Case_02"));
+ PinGroupList.Add(new ActionPinGroup(this, "Default"));
+
+ CustomListProperty caseList = new CustomListProperty
+ {
+ Name = "值列表",
+ ItemList = new List { "Case_01", "Case_02", "Default" },
+ ItemAdded = OnItemAdded,
+ ItemRemoved = OnItemRemoved,
+ ItemChanged = OnItemChanged
+ };
+ PropertyList.Add(caseList);
+
+ InitPinGroup();
+ }
+
+ protected override void ExecuteNode()
+ {
+ // 更新值
+ UpdateData(1);
+ // 获取动作引脚索引
+ int actionIndex = GetActionIndex(GetData(1));
+ // 执行引脚
+ if (actionIndex != -1) GetPinGroup(actionIndex + 2).Invoke();
+
+ GetPinGroup().Execute();
+ }
+
+ public override string GetTypeString() => nameof(Flow_Switch);
+
+ public override Dictionary GetParaDict() =>
+ new Dictionary { { "Value", GetData(1) } };
+
+ public override void LoadParaDict(string version, Dictionary paraDict)
+ {
+ try
+ {
+ SetData(1, paraDict["Value"]);
+ }
+ catch (Exception)
+ {
+ }
+ }
+
+ public override Dictionary GetPropertyDict()
+ {
+ return new Dictionary { { "CustomList", PropertyList[0].ToString() } };
+ }
+
+ public override void LoadPropertyDict(string version, Dictionary propertyDict)
+ {
+ try
+ {
+ PropertyList[0] = new CustomListProperty(propertyDict["CustomList"])
+ {
+ Name = "值列表",
+ ItemAdded = OnItemAdded,
+ ItemRemoved = OnItemRemoved,
+ ItemChanged = OnItemChanged
+ };
+ UpdateAllItem();
+ }
+ catch (Exception)
+ {
+ }
+ }
+
+ protected override NodeBase CloneNode() => new Flow_Switch();
+
+ #region 属性变更
+
+ private void OnItemAdded(string item)
+ {
+ ActionPinGroup group = new ActionPinGroup(this, item);
+ PinGroupList.Add(group);
+ group.Init();
+ // 通知引脚组变更
+ InvokePinGroupListChanged();
+ }
+
+ private void OnItemRemoved(int index)
+ {
+ // 引脚组索引
+ int groupIndex = index + 2;
+ if (PinGroupList.IndexOut(groupIndex)) return;
+ // 断开引脚
+ BreakPin(groupIndex);
+ // 移除引脚组
+ PinGroupList.RemoveAt(groupIndex);
+ // 通知引脚组变更
+ InvokePinGroupListChanged();
+ }
+
+ private void OnItemChanged(int index, string value)
+ {
+ int groupIndex = index + 2;
+ if (PinGroupList.IndexOut(groupIndex)) return;
+ ((ActionPinGroup)PinGroupList[groupIndex]).ActionName = value;
+ // 通知引脚组变更
+ InvokePinGroupListChanged();
+ }
+
+ #endregion
+
+ #region 私有方法
+
+ ///
+ /// 更新全部项
+ ///
+ private void UpdateAllItem()
+ {
+ while (PinGroupList.Count > 2) PinGroupList.RemoveLast();
+ foreach (var item in ((CustomListProperty)PropertyList[0]).ItemList)
+ {
+ ActionPinGroup group = new ActionPinGroup(this, item);
+ PinGroupList.Add(group);
+ group.Init();
+ }
+ }
+
+ private int GetActionIndex(string value)
+ {
+ int defaultIndex = ((CustomListProperty)PropertyList[0]).ItemList.IndexOf("Default");
+ int valueIndex = ((CustomListProperty)PropertyList[0]).ItemList.IndexOf(value);
+ return valueIndex == -1 ? defaultIndex : valueIndex;
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeLibSystem/Define/Flows/Flow_While.cs b/AXNode/SubSystem/NodeLibSystem/Define/Flows/Flow_While.cs
new file mode 100644
index 0000000..fed1fb7
--- /dev/null
+++ b/AXNode/SubSystem/NodeLibSystem/Define/Flows/Flow_While.cs
@@ -0,0 +1,78 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using XLib.Node;
+
+namespace AXNode.SubSystem.NodeLibSystem.Define.Flows
+{
+ public class Flow_While : NodeBase
+ {
+ public override void Init()
+ {
+ SetViewProperty(NodeColorSet.Flow, "Loop", "条件循环");
+
+ PinGroupList.Add(new ExecutePinGroup(this, "逻辑值为真,则无限循环执行"));
+ PinGroupList.Add(new DataPinGroup(this, "bool", "逻辑值", "True") { Readable = false });
+ PinGroupList.Add(new ActionPinGroup(this, "循环体"));
+
+ InitPinGroup();
+ }
+
+ protected override void ExecuteNode()
+ {
+ // 获取逻辑值引脚组
+ DataPinGroup logicValueGroup = (DataPinGroup)PinGroupList[1];
+ // 获取循环体引脚组
+ ActionPinGroup loopBodyGroup = GetPinGroup(2);
+
+ SynchronizationContext? context = SynchronizationContext.Current;
+ if (context == null)
+ {
+ context = new SynchronizationContext();
+ SynchronizationContext.SetSynchronizationContext(context);
+ }
+
+ Task.Run(() => ExecuteWhileSync(logicValueGroup, loopBodyGroup, context));
+ }
+
+ public override string GetTypeString() => nameof(Flow_While);
+
+ public override Dictionary GetParaDict() =>
+ new Dictionary { { "LogicValue", GetData(1) } };
+
+ protected override NodeBase CloneNode() => new Flow_While();
+
+ private void ExecuteWhileSync(DataPinGroup logicValueGroup, ActionPinGroup loopBodyGroup,
+ SynchronizationContext context)
+ {
+ try
+ {
+ while (true)
+ {
+ // 执行连接至逻辑值的节点
+ if (logicValueGroup.InputPin != null && logicValueGroup.InputPin.SourceList.Count > 0)
+ // 输入引脚.第一个源引脚.所属引脚组.所属节点.执行
+ logicValueGroup.InputPin.SourceList[0].OwnerGroup.OwnerNode.Execute();
+ // 更新逻辑值
+ UpdateData(1);
+ // 如果值为假,则退出循环
+ if (!bool.Parse(GetData(1))) break;
+ // 执行循环体
+ loopBodyGroup.Invoke();
+ }
+
+ context.Post(_ => GetPinGroup().Execute(), null);
+ }
+ catch (Exception ex)
+ {
+ context.Post(_ =>
+ {
+ RunError = true;
+ Stop();
+ InvokeExecuteError(ex);
+ }, null);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeLibSystem/Define/Functions/Func_BinOP.cs b/AXNode/SubSystem/NodeLibSystem/Define/Functions/Func_BinOP.cs
new file mode 100644
index 0000000..a03b4c7
--- /dev/null
+++ b/AXNode/SubSystem/NodeLibSystem/Define/Functions/Func_BinOP.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Collections.Generic;
+using XLib.Node;
+
+
+namespace AXNode.SubSystem.NodeLibSystem.Define.Functions
+{
+ public class Func_BinOP : NodeBase
+ {
+ public override void Init()
+ {
+ SetViewProperty(NodeColorSet.Function, "Function", "二元运算");
+
+ PinGroupList.Add(new ExecutePinGroup(this, "执行四则运算"));
+ PinGroupList.Add(new DataPinGroup(this, "double", "数值一", "0") { Readable = false });
+ PinGroupList.Add(new DataPinGroup(this, "string", "运算符", "+") { Readable = false, Writeable = false });
+ PinGroupList.Add(new DataPinGroup(this, "double", "数值二", "0") { Readable = false });
+ PinGroupList.Add(new DataPinGroup(this, "double", "结果", "0") { Writeable = false });
+
+ InitPinGroup();
+ }
+
+ protected override void ExecuteNode()
+ {
+ // 更新数值一、数值二
+ ((DataPinGroup)PinGroupList[1]).UpdateValue();
+ ((DataPinGroup)PinGroupList[3]).UpdateValue();
+ // 解析数值
+ double num1 = double.Parse(((DataPinGroup)PinGroupList[1]).Value);
+ double num2 = double.Parse(((DataPinGroup)PinGroupList[3]).Value);
+ // 执行对比
+ double result = 0d;
+ switch (((DataPinGroup)PinGroupList[2]).Value)
+ {
+ case "+":
+ result = num1 + num2;
+ break;
+ case "-":
+ result = num1 - num2;
+ break;
+ case "*":
+ result = num1 * num2;
+ break;
+ case "/":
+ result = num1 / num2;
+ break;
+ }
+
+ // 更新结果
+ ((DataPinGroup)PinGroupList[4]).Value = result.ToString();
+
+ ((ExecutePinGroup)PinGroupList[0]).Execute();
+ }
+
+ public override Dictionary GetParaDict()
+ {
+ Dictionary result = new Dictionary
+ {
+ { "Num1", GetData(1) },
+ { "Operator", GetData(2) },
+ { "Num2", GetData(3) },
+ { "Result", GetData(4) }
+ };
+ return result;
+ }
+
+ public override string GetTypeString() => nameof(Func_BinOP);
+
+ public override void LoadParaDict(string version, Dictionary paraDict)
+ {
+ try
+ {
+ SetData(1, paraDict["Num1"]);
+ SetData(2, paraDict["Operator"]);
+ SetData(3, paraDict["Num2"]);
+ SetData(4, paraDict["Result"]);
+ }
+ catch (Exception)
+ {
+ }
+ }
+
+
+ protected override NodeBase CloneNode() => new Func_Compare();
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeLibSystem/Define/Functions/Func_Compare.cs b/AXNode/SubSystem/NodeLibSystem/Define/Functions/Func_Compare.cs
new file mode 100644
index 0000000..e48592c
--- /dev/null
+++ b/AXNode/SubSystem/NodeLibSystem/Define/Functions/Func_Compare.cs
@@ -0,0 +1,90 @@
+using System;
+using System.Collections.Generic;
+using XLib.Node;
+
+namespace AXNode.SubSystem.NodeLibSystem.Define.Functions
+{
+ public class Func_Compare : NodeBase
+ {
+ public override void Init()
+ {
+ SetViewProperty(NodeColorSet.Function, "Function", "关系运算");
+
+ PinGroupList.Add(new ExecutePinGroup(this, "对比两个值的关系"));
+ PinGroupList.Add(new DataPinGroup(this, "double", "数值一", "0") { Readable = false });
+ PinGroupList.Add(new DataPinGroup(this, "string", "运算符", "=") { Readable = false, Writeable = false });
+ PinGroupList.Add(new DataPinGroup(this, "double", "数值二", "0") { Readable = false });
+ PinGroupList.Add(new DataPinGroup(this, "bool", "对比结果", "True") { Writeable = false });
+
+ InitPinGroup();
+ }
+
+ protected override void ExecuteNode()
+ {
+ // 更新数值一、数值二
+ ((DataPinGroup)PinGroupList[1]).UpdateValue();
+ ((DataPinGroup)PinGroupList[3]).UpdateValue();
+ // 解析数值
+ double num1 = double.Parse(((DataPinGroup)PinGroupList[1]).Value);
+ double num2 = double.Parse(((DataPinGroup)PinGroupList[3]).Value);
+ // 执行对比
+ bool result = false;
+ switch (((DataPinGroup)PinGroupList[2]).Value)
+ {
+ case "<":
+ result = num1 < num2;
+ break;
+ case "=":
+ result = num1 == num2;
+ break;
+ case ">":
+ result = num1 > num2;
+ break;
+ case "<=":
+ result = num1 < num2 || num1 == num2;
+ break;
+ case ">=":
+ result = num1 > num2 || num1 == num2;
+ break;
+ case "!=":
+ result = num1 != num2;
+ break;
+ }
+
+ // 更新结果
+ ((DataPinGroup)PinGroupList[4]).Value = result == true ? "True" : "False";
+
+ ((ExecutePinGroup)PinGroupList[0]).Execute();
+ }
+
+ public override string GetTypeString() => nameof(Func_Compare);
+
+ public override Dictionary GetParaDict()
+ {
+ Dictionary result = new Dictionary
+ {
+ { "Num1", GetData(1) },
+ { "Operator", GetData(2) },
+ { "Num2", GetData(3) },
+ { "Result", GetData(4) }
+ };
+ return result;
+ }
+
+ public override void LoadParaDict(string version, Dictionary paraDict)
+ {
+ try
+ {
+ SetData(1, paraDict["Num1"]);
+ SetData(2, paraDict["Operator"]);
+ SetData(3, paraDict["Num2"]);
+ SetData(4, paraDict["Result"]);
+ }
+ catch (Exception)
+ {
+ }
+ }
+
+ protected override NodeBase CloneNode() => new Func_Compare();
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeLibSystem/Define/Functions/Func_CreateThread.cs b/AXNode/SubSystem/NodeLibSystem/Define/Functions/Func_CreateThread.cs
new file mode 100644
index 0000000..576e764
--- /dev/null
+++ b/AXNode/SubSystem/NodeLibSystem/Define/Functions/Func_CreateThread.cs
@@ -0,0 +1,29 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using XLib.Node;
+
+namespace AXNode.SubSystem.NodeLibSystem.Define.Functions
+{
+ public class Func_CreateThread : NodeBase
+ {
+ public override void Init()
+ {
+ SetViewProperty(NodeColorSet.Function, "Function", "多线程执行");
+
+ PinGroupList.Add(new ExecutePinGroup(this, "创建新线程,并在新线程中执行后续节点"));
+
+ InitPinGroup();
+ }
+
+ protected override void ExecuteNode()
+ {
+ Task.Run(() => GetPinGroup().Execute());
+ }
+
+ public override string GetTypeString() => nameof(Func_CreateThread);
+
+ public override Dictionary GetParaDict() => new Dictionary();
+
+ protected override NodeBase CloneNode() => new Func_CreateThread();
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeLibSystem/Define/Functions/Func_Delay.cs b/AXNode/SubSystem/NodeLibSystem/Define/Functions/Func_Delay.cs
new file mode 100644
index 0000000..5585ac7
--- /dev/null
+++ b/AXNode/SubSystem/NodeLibSystem/Define/Functions/Func_Delay.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using XLib.Node;
+
+namespace AXNode.SubSystem.NodeLibSystem.Define.Functions
+{
+ public class Func_Delay : NodeBase
+ {
+ #region 生命周期
+
+ public override void Init()
+ {
+ SetViewProperty(NodeColorSet.Function, "Delay", "延迟执行");
+
+ PinGroupList.Add(new ExecutePinGroup(this, "延迟指定毫秒后执行"));
+ PinGroupList.Add(new DataPinGroup(this, "int", "时长", "5000") { Readable = false, Writeable = false });
+
+ InitPinGroup();
+ }
+
+ #endregion
+
+ #region NodeBase 方法
+
+ protected override async void ExecuteNode()
+ {
+ await Task.Delay(int.Parse(GetData(1)));
+ GetPinGroup().Execute();
+ }
+
+ public override string GetTypeString() => nameof(Func_Delay);
+
+ public override Dictionary GetParaDict()
+ {
+ return new Dictionary
+ {
+ { "Time", GetData(1) }
+ };
+ }
+
+ public override void LoadParaDict(string version, Dictionary paraDict)
+ {
+ try
+ {
+ SetData(1, paraDict["Time"]);
+ }
+ catch (Exception)
+ {
+ }
+ }
+
+ protected override NodeBase CloneNode() => new Func_Delay();
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeLibSystem/Define/Functions/Func_Log.cs b/AXNode/SubSystem/NodeLibSystem/Define/Functions/Func_Log.cs
new file mode 100644
index 0000000..2a2a2a7
--- /dev/null
+++ b/AXNode/SubSystem/NodeLibSystem/Define/Functions/Func_Log.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+using XLib.Node;
+
+namespace AXNode.SubSystem.NodeLibSystem.Define.Functions
+{
+ public class Func_Log : NodeBase
+ {
+ public override void Init()
+ {
+ SetViewProperty(NodeColorSet.Function, "Function", "日志");
+
+ PinGroupList.Add(new ExecutePinGroup(this, "发送消息至控制台"));
+ PinGroupList.Add(new DataPinGroup(this, "string", "消息", "Message") { BoxWidth = 180, Readable = false });
+
+ InitPinGroup();
+ }
+
+ protected override void ExecuteNode()
+ {
+ ((DataPinGroup)PinGroupList[1]).UpdateValue();
+
+ Console.WriteLine(((DataPinGroup)PinGroupList[1]).Value);
+
+ ((ExecutePinGroup)PinGroupList[0]).Execute();
+ }
+
+ public override string GetTypeString() => nameof(Func_Log);
+
+ public override Dictionary GetParaDict() =>
+ new Dictionary { { "Msg", GetData(1) } };
+
+ public override void LoadParaDict(string version, Dictionary paraDict)
+ {
+ try
+ {
+ SetData(1, paraDict["Msg"]);
+ }
+ catch (Exception)
+ {
+ }
+ }
+
+ protected override NodeBase CloneNode() => new Func_Log();
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeLibSystem/Define/Functions/Func_NumberToRatio.cs b/AXNode/SubSystem/NodeLibSystem/Define/Functions/Func_NumberToRatio.cs
new file mode 100644
index 0000000..463d8e8
--- /dev/null
+++ b/AXNode/SubSystem/NodeLibSystem/Define/Functions/Func_NumberToRatio.cs
@@ -0,0 +1,100 @@
+using System;
+using System.Collections.Generic;
+using XLib.Base.Ex;
+using XLib.Node;
+
+namespace AXNode.SubSystem.NodeLibSystem.Define.Functions
+{
+ public class Func_NumberToRatio : NodeBase
+ {
+ public override void Init()
+ {
+ SetViewProperty(NodeColorSet.Function, "Function", "数值转比例");
+
+ PinGroupList.Add(new ExecutePinGroup(this, "将指定范围内的数值转换为比例值"));
+ PinGroupList.Add(new DataPinGroup(this, "double", "最小值", "0") { Readable = false, Writeable = false });
+ PinGroupList.Add(new DataPinGroup(this, "double", "最大值", "100") { Readable = false, Writeable = false });
+ PinGroupList.Add(new DataPinGroup(this, "double", "当前值", "0") { Readable = false });
+ PinGroupList.Add(new DataPinGroup(this, "double", "转换结果", "0") { Readable = true, Writeable = false });
+
+ NodeProperty precision = new NodeProperty("int", "小数位数", "2");
+ precision.ValueChanged += Precision_ValueChanged;
+ PropertyList.Add(precision);
+
+ InitPinGroup();
+ }
+
+ protected override void ExecuteNode()
+ {
+ // 更新当前值
+ ((DataPinGroup)PinGroupList[3]).UpdateValue();
+ // 解析参数
+ double min = double.Parse(((DataPinGroup)PinGroupList[1]).Value);
+ double max = double.Parse(((DataPinGroup)PinGroupList[2]).Value);
+ double value = double.Parse(((DataPinGroup)PinGroupList[3]).Value).Limit(min, max);
+ // 计算结果
+ double result = (value - min) / (max - min);
+ // 设置结果
+ ((DataPinGroup)PinGroupList[4]).Value = result.ToString(_format);
+
+ ((ExecutePinGroup)PinGroupList[0]).Execute();
+ }
+
+ public override string GetTypeString() => nameof(Func_NumberToRatio);
+
+ public override Dictionary GetParaDict()
+ {
+ Dictionary result = new Dictionary
+ {
+ { "Min", GetData(1) },
+ { "Max", GetData(2) },
+ { "Current", GetData(3) },
+ { "Result", GetData(4) }
+ };
+ return result;
+ }
+
+ public override void LoadParaDict(string version, Dictionary paraDict)
+ {
+ try
+ {
+ SetData(1, paraDict["Min"]);
+ SetData(2, paraDict["Max"]);
+ SetData(3, paraDict["Current"]);
+ SetData(4, paraDict["Result"]);
+ }
+ catch (Exception)
+ {
+ }
+ }
+
+ public override Dictionary GetPropertyDict()
+ {
+ return new Dictionary
+ {
+ { "Precision", PropertyList[0].Value }
+ };
+ }
+
+ public override void LoadPropertyDict(string version, Dictionary propertyDict)
+ {
+ try
+ {
+ PropertyList[0].Value = propertyDict["Precision"];
+ }
+ catch (Exception)
+ {
+ }
+ }
+
+ protected override NodeBase CloneNode() => new Func_NumberToRatio();
+
+ private void Precision_ValueChanged(string value)
+ {
+ int count = int.Parse(value).Limit(0, 8);
+ _format = count == 0 ? "0" : "0." + new string('0', count);
+ }
+
+ private string _format = "0.00";
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeLibSystem/Define/Functions/Func_RatioToInt.cs b/AXNode/SubSystem/NodeLibSystem/Define/Functions/Func_RatioToInt.cs
new file mode 100644
index 0000000..268d7e4
--- /dev/null
+++ b/AXNode/SubSystem/NodeLibSystem/Define/Functions/Func_RatioToInt.cs
@@ -0,0 +1,71 @@
+using System;
+using System.Collections.Generic;
+using XLib.Node;
+
+namespace AXNode.SubSystem.NodeLibSystem.Define.Functions
+{
+ ///
+ /// 比例转整数
+ ///
+ public class Func_RatioToInt : NodeBase
+ {
+ public override void Init()
+ {
+ SetViewProperty(NodeColorSet.Function, "Function", "比例转整数");
+
+ PinGroupList.Add(new ExecutePinGroup(this, "将比例值转换为指定范围内的整数"));
+ PinGroupList.Add(new DataPinGroup(this, "int", "最小值", "0") { Readable = false, Writeable = false });
+ PinGroupList.Add(new DataPinGroup(this, "int", "最大值", "100") { Readable = false, Writeable = false });
+ PinGroupList.Add(new DataPinGroup(this, "double", "比例", "0") { Readable = false });
+ PinGroupList.Add(new DataPinGroup(this, "int", "转换结果", "0") { Readable = true, Writeable = false });
+
+ InitPinGroup();
+ }
+
+ protected override void ExecuteNode()
+ {
+ // 更新比例
+ ((DataPinGroup)PinGroupList[3]).UpdateValue();
+ // 解析参数
+ int min = int.Parse(((DataPinGroup)PinGroupList[1]).Value);
+ int max = int.Parse(((DataPinGroup)PinGroupList[2]).Value);
+ double ratio = double.Parse(((DataPinGroup)PinGroupList[3]).Value);
+ // 计算结果
+ int result = (int)(Math.Round((max - min) * ratio) + min);
+ // 设置结果
+ ((DataPinGroup)PinGroupList[4]).Value = result.ToString();
+
+ ((ExecutePinGroup)PinGroupList[0]).Execute();
+ }
+
+ public override string GetTypeString() => nameof(Func_RatioToInt);
+
+ public override Dictionary GetParaDict()
+ {
+ Dictionary result = new Dictionary
+ {
+ { "Min", GetData(1) },
+ { "Max", GetData(2) },
+ { "Current", GetData(3) },
+ { "Result", GetData(4) }
+ };
+ return result;
+ }
+
+ public override void LoadParaDict(string version, Dictionary paraDict)
+ {
+ try
+ {
+ SetData(1, paraDict["Min"]);
+ SetData(2, paraDict["Max"]);
+ SetData(3, paraDict["Current"]);
+ SetData(4, paraDict["Result"]);
+ }
+ catch (Exception)
+ {
+ }
+ }
+
+ protected override NodeBase CloneNode() => new Func_RatioToInt();
+ }
+}
\ No newline at end of file
diff --git a/AXNode/SubSystem/NodeLibSystem/Define/Functions/Func_SendNetMessage.cs b/AXNode/SubSystem/NodeLibSystem/Define/Functions/Func_SendNetMessage.cs
new file mode 100644
index 0000000..72c57fa
--- /dev/null
+++ b/AXNode/SubSystem/NodeLibSystem/Define/Functions/Func_SendNetMessage.cs
@@ -0,0 +1,157 @@
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using XLib.Node;
+
+namespace AXNode.SubSystem.NodeLibSystem.Define.Functions
+{
+ ///
+ /// 发送网络消息
+ ///
+ public class Func_SendNetMessage : NodeBase
+ {
+ public override void Init()
+ {
+ SetViewProperty(NodeColorSet.Function, "Function", "发送网络消息");
+
+ PinGroupList.Add(new ExecutePinGroup(this, "发送字符串至网络设备"));
+ PinGroupList.Add(new DataPinGroup(this, "string", "地址", "127.0.0.1:2400")
+ { BoxWidth = 120, Readable = false, Writeable = false });
+ PinGroupList.Add(new DataPinGroup(this, "string", "协议", "udp") { Readable = false, Writeable = false });
+ PinGroupList.Add(new DataPinGroup(this, "bool", "字节模式", "False") { Readable = false, Writeable = false });
+ PinGroupList.Add(new DataPinGroup(this, "string", "消息", "Hello World")
+ { BoxWidth = 200, Readable = false });
+
+ InitPinGroup();
+ }
+
+ protected override void ExecuteNode()
+ {
+ // 解析地址
+ IPEndPoint address = IPEndPoint.Parse(GetData(1));
+ // 获取协议
+ string protocol = GetData(2).ToUpper();
+ // 使用字节模式
+ bool byteMode = bool.Parse(GetData(3));
+ // 获取消息
+ UpdateData(4);
+ string message = GetData(4);
+ // 消息转字节
+ byte[]? messageByte = MessageToByteArray(message, byteMode);
+ if (messageByte == null) return;
+ // 发送消息
+ if (protocol == "UDP")
+ {
+ UdpClient udpClient = new UdpClient();
+ udpClient.Send(messageByte, messageByte.Length, address);
+
+ GetPinGroup().Execute();
+ }
+ else if (protocol == "TCP")
+ {
+ SynchronizationContext? context = SynchronizationContext.Current;
+ if (context == null)
+ {
+ context = new SynchronizationContext();
+ SynchronizationContext.SetSynchronizationContext(context);
+ }
+
+ Task.Run(() => SendTcpMessageSync(address, messageByte, context));
+ }
+ }
+
+ public override string GetTypeString() => nameof(Func_SendNetMessage);
+
+ public override Dictionary GetParaDict()
+ {
+ Dictionary result = new Dictionary
+ {
+ { "Address", GetData(1) },
+ { "Protocol", GetData(2) },
+ { "ByteMode", GetData(3) },
+ { "Message", GetData(4) }
+ };
+ return result;
+ }
+
+ public override void LoadParaDict(string version, Dictionary