docs: 添加 cyclegui-app-development 项目技能

将 CycleGUI 开发技能(含 Duplicated id 防碰撞规范)纳入 .cursor/skills,并调整 gitignore 仅放行 skills 目录可提交。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zhaowei.huang
2026-06-30 22:38:18 +08:00
co-authored by Cursor
parent 1724ea57f6
commit 158d770c54
12 changed files with 2149 additions and 1 deletions
@@ -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();
```
## 示例 7Painter 实时调试绘制
```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();
```
## 示例 10Medulla2 风格的 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);
}
```
## 示例 13Painter 填充多边形
```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);
```
## 示例 14Medulla2 插件结构
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
```