docs: 添加 cyclegui-app-development 项目技能
将 CycleGUI 开发技能(含 Duplicated id 防碰撞规范)纳入 .cursor/skills,并调整 gitignore 仅放行 skills 目录可提交。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,555 @@
|
||||
# CycleGUI 实用示例
|
||||
|
||||
## 示例 1:最小应用
|
||||
|
||||
最简可运行 CycleGUI 应用:
|
||||
|
||||
```csharp
|
||||
using CycleGUI;
|
||||
using CycleGUI.Terminals;
|
||||
|
||||
LocalTerminal.SetTitle("Hello CycleGUI");
|
||||
LocalTerminal.AddMenuItem("Exit", LocalTerminal.Terminate);
|
||||
LocalTerminal.Start();
|
||||
|
||||
GUI.PromptPanel(pb =>
|
||||
{
|
||||
pb.Panel.ShowTitle("Hello");
|
||||
pb.Label("Hello from CycleGUI!");
|
||||
if (pb.Button("Exit"))
|
||||
pb.Panel.Exit();
|
||||
});
|
||||
```
|
||||
|
||||
## 示例 2:多面板导航(LearnCycleGUI 模式)
|
||||
|
||||
主面板按钮打开子面板,子面板停靠在左侧:
|
||||
|
||||
```csharp
|
||||
Panel mainPanel = null;
|
||||
Panel settingsPanel = null;
|
||||
List<Panel> activePanels = new();
|
||||
|
||||
mainPanel = GUI.PromptPanel(pb =>
|
||||
{
|
||||
pb.Panel.ShowTitle("Main");
|
||||
|
||||
if (pb.Button("Open Settings"))
|
||||
{
|
||||
if (!activePanels.Contains(settingsPanel))
|
||||
{
|
||||
settingsPanel = GUI.DeclarePanel()
|
||||
.ShowTitle("Settings")
|
||||
.InitPosRelative(mainPanel, 0, 16, 0, 1)
|
||||
.SetDefaultDocking(Panel.Docking.Left);
|
||||
settingsPanel.Define(CreateSettingsHandler());
|
||||
activePanels.Add(settingsPanel);
|
||||
}
|
||||
else
|
||||
settingsPanel.BringToFront();
|
||||
}
|
||||
});
|
||||
|
||||
PanelBuilder.CycleGUIHandler CreateSettingsHandler()
|
||||
{
|
||||
float volume = 0.5f;
|
||||
bool darkMode = true;
|
||||
int quality = 1;
|
||||
|
||||
return pb =>
|
||||
{
|
||||
pb.SliderFloat("Volume", ref volume, 0, 1);
|
||||
pb.Toggle("Dark Mode", ref darkMode);
|
||||
pb.RadioButtons("Quality", new[] { "Low", "Medium", "High" }, ref quality);
|
||||
|
||||
pb.Separator();
|
||||
if (pb.Closing())
|
||||
{
|
||||
activePanels.Remove(settingsPanel);
|
||||
pb.Panel.Exit();
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## 示例 3:模态对话框
|
||||
|
||||
阻塞式确认对话框,返回用户选择:
|
||||
|
||||
```csharp
|
||||
bool confirmed = false;
|
||||
|
||||
GUI.PromptAndWaitPanel(pb =>
|
||||
{
|
||||
pb.Panel.ShowTitle("Confirm").Modal(true).AutoSize(true);
|
||||
pb.Label("Are you sure you want to delete?");
|
||||
pb.SameLine();
|
||||
if (pb.Button("Yes", distinct: "yes"))
|
||||
{
|
||||
confirmed = true;
|
||||
pb.Panel.Exit();
|
||||
}
|
||||
pb.SameLine();
|
||||
if (pb.Button("No", distinct: "no"))
|
||||
{
|
||||
pb.Panel.Exit();
|
||||
}
|
||||
});
|
||||
|
||||
if (confirmed) { /* proceed */ }
|
||||
```
|
||||
|
||||
## 示例 4:实时数据面板
|
||||
|
||||
后台线程更新数据,面板实时刷新:
|
||||
|
||||
```csharp
|
||||
float temperature = 20f;
|
||||
float humidity = 50f;
|
||||
bool running = true;
|
||||
|
||||
var panel = GUI.DeclarePanel()
|
||||
.ShowTitle("Sensor Monitor")
|
||||
.InitSize(350, 200)
|
||||
.SetDefaultDocking(Panel.Docking.Right);
|
||||
|
||||
panel.Define(pb =>
|
||||
{
|
||||
pb.MiniPlot("Temperature", temperature);
|
||||
pb.MiniPlot("Humidity", humidity);
|
||||
pb.RealtimePlot("Temp Chart", temperature);
|
||||
|
||||
pb.Separator();
|
||||
if (pb.Button("Stop"))
|
||||
running = false;
|
||||
|
||||
pb.Panel.Repaint(); // 实时数据:handler 内持续 Repaint
|
||||
});
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
var rng = new Random();
|
||||
while (running)
|
||||
{
|
||||
temperature = 20f + (float)rng.NextDouble() * 5;
|
||||
humidity = 50f + (float)rng.NextDouble() * 10;
|
||||
Thread.Sleep(100);
|
||||
// 也可在此 panel.Repaint() 替代 handler 内 Repaint,二选一
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## 示例 5:表格展示与交互
|
||||
|
||||
```csharp
|
||||
string[] names = { "Alice", "Bob", "Charlie", "Diana" };
|
||||
float[] scores = { 95, 87, 73, 91 };
|
||||
bool[] enabled = { true, true, false, true };
|
||||
|
||||
panel.Define(pb =>
|
||||
{
|
||||
pb.Table("students", new[] { "Name", "Score", "Enabled", "Action" },
|
||||
names.Length, (row, i) =>
|
||||
{
|
||||
row.Label(names[i]);
|
||||
row.Label(scores[i].ToString("F1"));
|
||||
row.Checkbox(ref enabled[i]);
|
||||
if (row.ButtonGroup("", new[] { "Edit", "Delete" }, out int act))
|
||||
{
|
||||
if (act == 0) Console.WriteLine($"Edit {names[i]}");
|
||||
if (act == 1) Console.WriteLine($"Delete {names[i]}");
|
||||
}
|
||||
}, height: 200, enableSearch: true, title: "Student Records");
|
||||
});
|
||||
```
|
||||
|
||||
## 示例 6:3D 场景 - 加载模型和点云
|
||||
|
||||
```csharp
|
||||
using CycleGUI;
|
||||
using CycleGUI.API;
|
||||
using System.Numerics;
|
||||
|
||||
// 加载模型类
|
||||
Workspace.AddProp(new LoadModel
|
||||
{
|
||||
name = "car_model",
|
||||
detail = new Workspace.ModelDetail(File.ReadAllBytes("car.glb"))
|
||||
{
|
||||
Scale = 0.001f,
|
||||
Center = new Vector3(0, 0, 0),
|
||||
}
|
||||
});
|
||||
|
||||
// 放置两个实例
|
||||
Workspace.AddProp(new PutModelObject
|
||||
{
|
||||
name = "car_a",
|
||||
clsName = "car_model",
|
||||
newPosition = new Vector3(0, 0, 0),
|
||||
newQuaternion = Quaternion.Identity,
|
||||
});
|
||||
|
||||
Workspace.AddProp(new PutModelObject
|
||||
{
|
||||
name = "car_b",
|
||||
clsName = "car_model",
|
||||
newPosition = new Vector3(5, 0, 0),
|
||||
newQuaternion = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathF.PI / 2),
|
||||
});
|
||||
|
||||
// 添加地面点云
|
||||
var gridPoints = new List<Vector4>();
|
||||
var gridColors = new List<uint>();
|
||||
for (float x = -10; x <= 10; x += 0.5f)
|
||||
for (float y = -10; y <= 10; y += 0.5f)
|
||||
{
|
||||
gridPoints.Add(new Vector4(x, y, 0, 2));
|
||||
gridColors.Add(0xFF808080);
|
||||
}
|
||||
|
||||
Workspace.AddProp(new PutPointCloud
|
||||
{
|
||||
name = "ground_grid",
|
||||
xyzSzs = gridPoints.ToArray(),
|
||||
colors = gridColors.ToArray(),
|
||||
newPosition = Vector3.Zero,
|
||||
});
|
||||
|
||||
// 设置相机
|
||||
new SetCamera
|
||||
{
|
||||
lookAt = new Vector3(2.5f, 0, 0),
|
||||
altitude = MathF.PI / 4,
|
||||
azimuth = -MathF.PI / 3,
|
||||
distance = 15f,
|
||||
}.IssueToAllTerminals();
|
||||
```
|
||||
|
||||
## 示例 7:Painter 实时调试绘制
|
||||
|
||||
```csharp
|
||||
var painter = Painter.GetPainter("debug_overlay");
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
float t = 0;
|
||||
while (true)
|
||||
{
|
||||
painter.Clear();
|
||||
|
||||
// 绘制坐标轴
|
||||
painter.DrawLine(Color.Red, Vector3.Zero, Vector3.UnitX * 2, 2f,
|
||||
Painter.ArrowType.End);
|
||||
painter.DrawLine(Color.Green, Vector3.Zero, Vector3.UnitY * 2, 2f,
|
||||
Painter.ArrowType.End);
|
||||
painter.DrawLine(Color.Blue, Vector3.Zero, Vector3.UnitZ * 2, 2f,
|
||||
Painter.ArrowType.End);
|
||||
|
||||
// 绘制旋转点
|
||||
float x = MathF.Cos(t) * 3;
|
||||
float y = MathF.Sin(t) * 3;
|
||||
painter.DrawDot(Color.Yellow, new Vector3(x, y, 0), 5f);
|
||||
painter.DrawText(Color.White, new Vector3(x, y, 0.3f),
|
||||
$"({x:F1}, {y:F1})");
|
||||
|
||||
// 绘制轨迹线
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
float t1 = t - i * 0.1f;
|
||||
float t2 = t - (i + 1) * 0.1f;
|
||||
painter.DrawLine(Color.FromArgb(255 - i * 12, 255, 255, 0),
|
||||
new Vector3(MathF.Cos(t1) * 3, MathF.Sin(t1) * 3, 0),
|
||||
new Vector3(MathF.Cos(t2) * 3, MathF.Sin(t2) * 3, 0));
|
||||
}
|
||||
|
||||
t += 0.05f;
|
||||
Thread.Sleep(33);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## 示例 8:对象选择与 Guizmo 操作
|
||||
|
||||
```csharp
|
||||
SelectObject selectOp = null;
|
||||
GuizmoAction guizmoOp = null;
|
||||
string selectedObject = null;
|
||||
|
||||
void StartSelect()
|
||||
{
|
||||
selectOp = new SelectObject();
|
||||
selectOp.feedback = (results, op) =>
|
||||
{
|
||||
if (results.Length > 0)
|
||||
{
|
||||
selectedObject = results[0].name;
|
||||
Console.WriteLine($"Selected: {selectedObject}");
|
||||
}
|
||||
};
|
||||
selectOp.Start();
|
||||
}
|
||||
|
||||
void StartGuizmo()
|
||||
{
|
||||
if (selectedObject == null) return;
|
||||
selectOp?.End();
|
||||
|
||||
guizmoOp = new GuizmoAction
|
||||
{
|
||||
dof = GuizmoAction.GuizmoDof.PlanarXYYaw,
|
||||
realtimeResult = true,
|
||||
};
|
||||
guizmoOp.feedback = (results, op) =>
|
||||
{
|
||||
foreach (var (name, pos, rot) in results)
|
||||
Console.WriteLine($"Moved {name} to {pos}");
|
||||
};
|
||||
guizmoOp.finished = () => StartSelect();
|
||||
guizmoOp.Start();
|
||||
}
|
||||
```
|
||||
|
||||
## 示例 9:拾取坐标绘制线段
|
||||
|
||||
```csharp
|
||||
Vector3? lineStart = null;
|
||||
|
||||
var getPos = new GetPosition
|
||||
{
|
||||
Name = "Draw Line",
|
||||
method = GetPosition.PickMode.GridPlane,
|
||||
};
|
||||
|
||||
getPos.feedback = (wp, op) =>
|
||||
{
|
||||
if (lineStart == null)
|
||||
{
|
||||
lineStart = wp.mouse_pos;
|
||||
}
|
||||
else
|
||||
{
|
||||
Workspace.AddProp(new PutStraightLine
|
||||
{
|
||||
name = $"line_{DateTime.Now.Ticks}",
|
||||
start = lineStart.Value,
|
||||
end = wp.mouse_pos,
|
||||
color = Color.Cyan,
|
||||
width = 2,
|
||||
arrowType = Painter.ArrowType.End,
|
||||
});
|
||||
lineStart = null;
|
||||
}
|
||||
};
|
||||
|
||||
getPos.Start();
|
||||
```
|
||||
|
||||
## 示例 10:Medulla2 风格的 IOObject 面板
|
||||
|
||||
Medulla2 中每个设备对象(IOObject)通过 `OpenUI` 打开独立的控制面板:
|
||||
|
||||
```csharp
|
||||
public class Camera3DIOObject : IOObject
|
||||
{
|
||||
private Panel uiPanel;
|
||||
private bool displaying = false;
|
||||
|
||||
public void OpenUI()
|
||||
{
|
||||
displaying = true;
|
||||
uiPanel = GUI.DeclarePanel()
|
||||
.ShowTitle($"Camera: {Name}")
|
||||
.SetDefaultDocking(Panel.Docking.Right);
|
||||
uiPanel.Define(pb =>
|
||||
{
|
||||
if (pb.Button("Capture")) TakeSnapshot();
|
||||
pb.Toggle("Live View", ref displaying);
|
||||
pb.MiniPlot("FPS", currentFps);
|
||||
|
||||
if (pb.Closing())
|
||||
{
|
||||
displaying = false;
|
||||
Painter.GetPainter(Name).Clear();
|
||||
uiPanel.Exit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void CloseUI()
|
||||
{
|
||||
displaying = false;
|
||||
Painter.GetPainter(Name).Clear();
|
||||
uiPanel?.Exit();
|
||||
}
|
||||
|
||||
public void draw()
|
||||
{
|
||||
if (!displaying) return;
|
||||
var pp = Painter.GetPainter(Name);
|
||||
pp.Clear();
|
||||
foreach (var pt in cachedPoints)
|
||||
{
|
||||
var cc = Color.FromArgb(pt.r, pt.g, pt.b);
|
||||
pp.DrawDot(cc, new Vector3(pt.X, pt.Y, pt.Z) / 1000f, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 示例 11:工具栏面板
|
||||
|
||||
```csharp
|
||||
var toolbar = GUI.DeclarePanel()
|
||||
.ShowTitle(null)
|
||||
.AsToolbarPanel(38, Panel.ToolbarAnchor.RightTop);
|
||||
|
||||
toolbar.Define(pb =>
|
||||
{
|
||||
var tpb = new ToolbarPanelBuilder(pb);
|
||||
tpb.Label($"{IconFonts.ForkAwesome.Cube} Scene");
|
||||
tpb.Separator();
|
||||
|
||||
if (tpb.Button($"{IconFonts.ForkAwesome.Play} Run", distinct: "tb-run"))
|
||||
StartSimulation();
|
||||
|
||||
if (tpb.Button($"{IconFonts.ForkAwesome.Stop} Stop", distinct: "tb-stop"))
|
||||
StopSimulation();
|
||||
|
||||
tpb.PopMenuButton($"{IconFonts.ForkAwesome.Camera} View", new[]
|
||||
{
|
||||
new MenuItem("Front", () => new SetCamera { azimuth = 0, altitude = MathF.PI/2 }.IssueToDefault()),
|
||||
new MenuItem("Top", () => new SetCamera { azimuth = 0, altitude = 0 }.IssueToDefault()),
|
||||
new MenuItem("Reset", () => new SetCamera().IssueToDefault()),
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## 示例 12:同时启用 Local + Web 终端
|
||||
|
||||
```csharp
|
||||
static void Main(string[] args)
|
||||
{
|
||||
LocalTerminal.SetTitle("Dual Terminal App");
|
||||
LocalTerminal.SetIcon(LoadIcon(), "DualApp");
|
||||
LocalTerminal.AddMenuItem("Exit", LocalTerminal.Terminate);
|
||||
LocalTerminal.Start();
|
||||
|
||||
// Web 终端在后台启动
|
||||
Task.Run(() =>
|
||||
{
|
||||
var htdocs = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "htdocs");
|
||||
if (Directory.Exists(htdocs))
|
||||
LeastServer.AddServingFiles("/", htdocs);
|
||||
|
||||
// 添加自定义 API
|
||||
LeastServer.AddGetHandler("/api/status", () =>
|
||||
JsonConvert.SerializeObject(new { status = "running", time = DateTime.Now }));
|
||||
|
||||
WebTerminal.Use(port: 8081, ico: LoadIcon());
|
||||
});
|
||||
|
||||
// 远程连接时显示的面板
|
||||
Terminal.RegisterRemotePanel(terminal =>
|
||||
CreateMainPanel(terminal));
|
||||
|
||||
// 本地面板
|
||||
GUI.PromptPanel(CreateMainPanel(GUI.defaultTerminal));
|
||||
}
|
||||
|
||||
static PanelBuilder.CycleGUIHandler CreateMainPanel(Terminal t)
|
||||
{
|
||||
return pb =>
|
||||
{
|
||||
pb.Panel.ShowTitle("Control Panel");
|
||||
pb.Label($"Terminal: {t.GetType().Name}");
|
||||
// 同样的 UI 在两个终端上都可见...
|
||||
};
|
||||
}
|
||||
|
||||
// 图标统一用 .ico 字节(也可 File.ReadAllBytes 从磁盘读)
|
||||
// 需 csproj: <EmbeddedResource Include="app_icon.ico" />;exe 图标另用 <ApplicationIcon>
|
||||
static byte[] LoadIcon()
|
||||
{
|
||||
var asm = Assembly.GetExecutingAssembly();
|
||||
using var s = asm.GetManifestResourceStream(
|
||||
asm.GetManifestResourceNames().First(p => p.Contains(".ico")));
|
||||
return new BinaryReader(s).ReadBytes((int)s.Length);
|
||||
}
|
||||
```
|
||||
|
||||
## 示例 13:Painter 填充多边形
|
||||
|
||||
```csharp
|
||||
var painter = Painter.GetPainter("polygon_demo");
|
||||
painter.Clear();
|
||||
|
||||
// 绘制地面上的三角形
|
||||
var triangle = new[]
|
||||
{
|
||||
new Vector2(0, 0),
|
||||
new Vector2(1, 0),
|
||||
new Vector2(0.5f, 0.866f),
|
||||
};
|
||||
painter.DrawPolygonFilled(triangle,
|
||||
trans: new Vector3(0, 0, 0),
|
||||
quat: Quaternion.Identity,
|
||||
borderColor: Color.White,
|
||||
fillColor: Color.FromArgb(128, Color.Green));
|
||||
|
||||
// 拉伸为柱体
|
||||
painter.DrawPolygonFilled(triangle,
|
||||
trans: new Vector3(3, 0, 0),
|
||||
quat: Quaternion.Identity,
|
||||
borderColor: Color.Yellow,
|
||||
fillColor: Color.FromArgb(100, Color.Blue),
|
||||
thickness: 1.5f);
|
||||
```
|
||||
|
||||
## 示例 14:Medulla2 插件结构
|
||||
|
||||
Medulla2 插件 DLL 必须暴露 `MainIOObject` 类:
|
||||
|
||||
```csharp
|
||||
// MyPlugin/MainIOObject.cs
|
||||
public class MainIOObject : IOObject
|
||||
{
|
||||
public object init(string typeName, dynamic[] args)
|
||||
{
|
||||
// 根据 typeName 创建设备实例
|
||||
if (typeName == "sensor_a")
|
||||
return new SensorA((string)args[0], (int)args[1]);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// MyPlugin/SensorA.cs
|
||||
public class SensorA : IOObject
|
||||
{
|
||||
public SensorA(string port, int baudRate) { /* init */ }
|
||||
|
||||
[IOObjectUtility]
|
||||
public void Calibrate() { /* 显示为 UI 按钮 */ }
|
||||
|
||||
[IOObjectMonitor]
|
||||
public float Temperature => ReadTemp();
|
||||
}
|
||||
```
|
||||
|
||||
csproj 引用 refasmer 生成的参考程序集:
|
||||
```xml
|
||||
<Reference Include="RefMedullaCore">
|
||||
<HintPath>$(ReleaseDir)\RefMedullaCore.dll</HintPath>
|
||||
</Reference>
|
||||
```
|
||||
|
||||
输出到插件目录:
|
||||
```xml
|
||||
<OutputPath>$(DebugDir)\plugins</OutputPath>
|
||||
```
|
||||
|
||||
加载方式(在 .iocmd 脚本中):
|
||||
```
|
||||
sensor = io load plugins/MyPlugin.dll
|
||||
mySensor = sensor init sensor_a COM3 115200
|
||||
```
|
||||
Reference in New Issue
Block a user