Files
StandardSence/.cursor/skills/cyclegui-app-development/references/examples.md
T
zhaowei.huangandCursor 158d770c54 docs: 添加 cyclegui-app-development 项目技能
将 CycleGUI 开发技能(含 Duplicated id 防碰撞规范)纳入 .cursor/skills,并调整 gitignore 仅放行 skills 目录可提交。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 22:38:18 +08:00

13 KiB
Raw Blame History

CycleGUI 实用示例

示例 1:最小应用

最简可运行 CycleGUI 应用:

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 模式)

主面板按钮打开子面板,子面板停靠在左侧:

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:模态对话框

阻塞式确认对话框,返回用户选择:

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:实时数据面板

后台线程更新数据,面板实时刷新:

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:表格展示与交互

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 场景 - 加载模型和点云

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 实时调试绘制

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 操作

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:拾取坐标绘制线段

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 打开独立的控制面板:

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:工具栏面板

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 终端

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 填充多边形

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 类:

// 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 生成的参考程序集:

<Reference Include="RefMedullaCore">
  <HintPath>$(ReleaseDir)\RefMedullaCore.dll</HintPath>
</Reference>

输出到插件目录:

<OutputPath>$(DebugDir)\plugins</OutputPath>

加载方式(在 .iocmd 脚本中):

sensor = io load plugins/MyPlugin.dll
mySensor = sensor init sensor_a COM3 115200