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,99 @@
|
||||
---
|
||||
name: cyclegui-app-development
|
||||
description: 构建或修改基于 CycleGUI 框架的 .NET 桌面/Web 应用。CycleGUI 提供 ImGui 式立即模式 UI 面板和 3D Workspace 渲染能力。用于用户提到 CycleGUI、RefCycleGUI.dll、Costura.Fody、SimpleLite、PanelBuilder、GUI.PromptPanel、GUI.DeclarePanel、Workspace、Painter、LocalTerminal、LocalTerminal.Start、MinimizeToTray、HideConsoleOnStart、libVRender、WebTerminal、TCPTerminal、3D 场景、立即模式 UI、LoadModel、PutPointCloud,或需要新建/修改 CycleGUI 应用、示例、面板、工具栏、HoverMenu、菜单栏、视口、对象选择、坐标拾取、调试绘制时。
|
||||
---
|
||||
|
||||
# CycleGUI 应用开发
|
||||
|
||||
本 skill 是 CycleGUI 应用开发的启动卡片。普通任务应能在没有 CycleGUI 源码的情况下完成:先用本文搭出正确结构;需要完整签名、长示例或专题规则时,再按主题加载 reference。只有维护 CycleGUI 框架本体、修复底层 bug、或本地仓库已存在且需要核对变更时,才读取源码。
|
||||
|
||||
## 按需读取
|
||||
|
||||
- 项目引用、宿主/插件边界、启动流程:`references/project-setup-and-packaging.md`
|
||||
- 面板、工具栏、HoverMenu、控件、菜单栏:`references/panels-controls-menus.md`
|
||||
- Workspace 模型、点云、相机、视口、Painter:`references/workspace-scene-and-painter.md`
|
||||
- Workspace 对象选择、坐标拾取、async 操作陷阱:`references/workspace-interaction.md`
|
||||
- WebTerminal、LocalTerminal、LeastServer:`references/terminals-and-web.md`
|
||||
- 完整 API 签名:`references/api-signatures.md`
|
||||
- 完整示例:`references/examples.md`
|
||||
|
||||
## 引用策略
|
||||
|
||||
先判断项目类型:
|
||||
|
||||
1. **同解决方案示例**:例如 `LearnCycleGUI`,可以 `ProjectReference` 到 `CycleGUI\CycleGUI.csproj`。
|
||||
2. **宿主/调度程序**:例如 `SimpleLite`,引用真实 `CycleGUI.dll`,并用 `Costura.Fody` 把托管程序集包进宿主 Assembly。
|
||||
3. **插件或二次开发项目**:默认引用 `RefCycleGUI.dll`,不要引用真实 `CycleGUI.dll`。真实 CycleGUI 实现由宿主提供,用于保护 CycleGUI 代码资产并隔离二开项目。
|
||||
|
||||
最容易写错的是插件/二开项目:
|
||||
|
||||
```xml
|
||||
<Reference Include="CycleGUI">
|
||||
<HintPath>$(CGUILibDir)RefCycleGUI.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
```
|
||||
|
||||
不要指导插件二开项目显式部署或打包真实 `CycleGUI.dll`。
|
||||
|
||||
## 最小骨架
|
||||
|
||||
常用 using:
|
||||
|
||||
```csharp
|
||||
using CycleGUI;
|
||||
using CycleGUI.API;
|
||||
using CycleGUI.Terminals;
|
||||
using System.Drawing;
|
||||
using System.Numerics;
|
||||
```
|
||||
|
||||
启动本地窗口:
|
||||
|
||||
```csharp
|
||||
Terminal.RegisterRemotePanel(CreateMainPanel);
|
||||
LocalTerminal.SetTitle("MyApp");
|
||||
LocalTerminal.Start();
|
||||
GUI.PromptPanel(CreateMainPanel(GUI.defaultTerminal));
|
||||
```
|
||||
|
||||
最小面板:
|
||||
|
||||
```csharp
|
||||
PanelBuilder.CycleGUIHandler CreateMainPanel(Terminal terminal)
|
||||
{
|
||||
return pb =>
|
||||
{
|
||||
pb.Panel.ShowTitle("Main").InitSize(400, 300);
|
||||
pb.Label("Ready");
|
||||
if (pb.Button("Run")) { }
|
||||
if (pb.Closing()) pb.Panel.Exit();
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
需要保留 panel 引用、控制 dock/toolbar/单实例时,用 `GUI.DeclarePanel().Define(...)`;细节见 `references/panels-controls-menus.md`。
|
||||
|
||||
## 常见任务路由
|
||||
|
||||
- 新建普通桌面/Web 应用:用本文骨架;需要完整 csproj 和 WebTerminal 细节时读 `references/project-setup-and-packaging.md`、`references/terminals-and-web.md`。
|
||||
- 做插件二开:先确认引用 `RefCycleGUI.dll`;不要把真实 `CycleGUI.dll` 带进插件。
|
||||
- 做表单、表格、toolbar、HoverMenu、菜单栏:读 `references/panels-controls-menus.md`。
|
||||
- 加载模型、点云、相机、Painter 调试绘制:读 `references/workspace-scene-and-painter.md`。
|
||||
- 做对象选择、坐标拾取、拖拽/放置或 async 交互:读 `references/workspace-interaction.md`。
|
||||
- 不确定 API 签名:读 `references/api-signatures.md`,不要凭 ImGui 习惯猜 CycleGUI 方法。
|
||||
|
||||
## 必须记住
|
||||
|
||||
- 普通应用开发不要求用户或下游 agent 读取 CycleGUI 源码。
|
||||
- CycleGUI 公开颜色字段使用 `System.Drawing.Color`;不要把 ARGB `uint` 直接塞给颜色字段。
|
||||
- 所有 `namePattern` 是 glob,不是 regex:`"UISite-*"` 正确,`"UISite-.*"` 错。
|
||||
- `InitPosRelative` 的参数是 `left` / `top`;不要写不存在的 `offsetX` / `offsetY`。
|
||||
- CycleGUI 没有 `BeginChild` / `ChildWindow` / `BeginTabBar` / `BeginTabItem`;分区用 `Table(height:)`、`CollapsingHeader`,标签页用 `TabButtons`(支持 `forceSelect` 程序化切 Tab;`Table` 支持 `scrollToRow` 程序化滚动到行,变化检测在 API 内部)。
|
||||
- 不要依赖空壳控件:`Progress`、`BulletText`、`Indent` / `UnIndent`、`QuestionMark`、`ToolTip`。
|
||||
- 控件 id 由标签经 **ASCII** 哈希得到(中文、全角符号等非 ASCII 字符全部折叠成 `?`):同一面板内两个「ASCII 折叠后相同」的标签会抛 `Duplicated id`——最典型是**同字数的纯中文**标签(如 `筛选条件` 撞 `设为类型`),或仅中文字符不同、ASCII 部分相同的两个标签(如 `起点筛选(站点ID或名称)` 撞 `终点筛选(站点ID或名称)`)。修法:标签加 `###唯一ASCII后缀`(显示不变,`###` 会重置哈希),`Button` 用唯一 `distinct:`,或加 `1. `/`2. ` 等数字前缀;循环内渲染带状态控件须给每项拼唯一 id(如 `$"启用###en-{item.Id}"`)。详见 `references/panels-controls-menus.md`。
|
||||
- 立即模式:面板默认**不会**每帧重绘;只在 `pb.Panel.Repaint()` 被调用时重新执行 handler。静态面板(Properties / Actions 等)不要无条件 Repaint;仅实时数据区(Status、MiniPlot、后台线程推送等)才持续 Repaint。延迟回调或跨线程改数据后须显式 Repaint。详见 `references/panels-controls-menus.md`。
|
||||
- 画布/Workspace 里的业务对象需要点击或选择时,默认用原生 `SelectObject` + 可选 Workspace 对象,不要用 `GetPosition` 手写命中测试。
|
||||
- `SelectObject` 和 `GetPosition` 是互斥交互;进入拾取前结束选择 op,结束后重新启动选择 op。
|
||||
- 修改 CycleGUI C# API、维护框架本体、排查 libVRender/协议问题、或发现 reference 缺失时,才核对源码并同步更新 references。
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
interface:
|
||||
display_name: "CycleGUI App Development"
|
||||
short_description: "Build CycleGUI panels, HoverMenus, and 3D workspace apps"
|
||||
default_prompt: "Use $cyclegui-app-development to build or modify a CycleGUI panel, HoverMenu, workspace, viewport, or demo app."
|
||||
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
@@ -0,0 +1,8 @@
|
||||
# CycleGUI Examples
|
||||
|
||||
完整示例已整理到 `references/examples.md`。优先按任务主题读取:
|
||||
|
||||
- 最小应用、多面板、模态、实时数据、表格:`references/examples.md` 示例 1-5。
|
||||
- 模型、点云、Painter、选择、坐标拾取:`references/examples.md` 示例 6-9、13。
|
||||
- 工具栏、Local + Web:`references/examples.md` 示例 11-12。
|
||||
- Medulla2 插件结构:`references/examples.md` 示例 10、14。
|
||||
@@ -0,0 +1,18 @@
|
||||
# CycleGUI Reference Index
|
||||
|
||||
按需读取主题文件,不要一次加载所有 reference。
|
||||
|
||||
## 主题文件
|
||||
|
||||
- `references/project-setup-and-packaging.md`:项目类型、`RefCycleGUI.dll`、`CycleGUI.dll`、Costura、启动流程。
|
||||
- `references/panels-controls-menus.md`:Panel 生命周期、定位、toolbar、HoverMenu、PanelBuilder、菜单栏。
|
||||
- `references/workspace-scene-and-painter.md`:模型、点云、线、相机、外观、Viewport、Painter。
|
||||
- `references/workspace-interaction.md`:对象选择、sticky pattern、坐标拾取、WorkspaceUIOperation async 包装。
|
||||
- `references/terminals-and-web.md`:LocalTerminal、WebTerminal、TCPTerminal、LeastServer。
|
||||
- `references/api-signatures.md`:完整 API 签名和维护者核对路径。
|
||||
- `references/examples.md`:完整示例代码。
|
||||
|
||||
## 选择规则
|
||||
|
||||
普通应用开发优先使用 `SKILL.md`。只有任务需要某个领域的完整签名、长示例或排障细节时,再读取对应主题文件。
|
||||
|
||||
@@ -0,0 +1,643 @@
|
||||
# CycleGUI API 参考
|
||||
|
||||
> **颜色字段统一为 `System.Drawing.Color`**(2026-05 改造):所有公开 API 的 color/colors/_shine/_border_color/team_color/shine_color 字段都是 `Color`,不再有 `uint` 入口。详见 `SKILL.md` `## 颜色契约` 章节。如果你拿到的是引擎 wire-format `uint`(bit0..7=R),用 `Extensions.FromRgba8(uint)` 升回 `Color`;System.Drawing ARGB 布局(`0xAARRGGBB`)则用 `Color.FromArgb(unchecked((int)argb))`。
|
||||
|
||||
## PanelBuilder 控件完整签名
|
||||
|
||||
维护者核对路径:`D:\MDCS\Source\Core\CycleGUI\CycleGUI\PanelBuilder.Controls.cs`
|
||||
|
||||
### 布局
|
||||
|
||||
```csharp
|
||||
void Label(string text)
|
||||
void Separator()
|
||||
void SeparatorText(string text)
|
||||
void SameLine(int spacing = 0)
|
||||
void CollapsingHeaderStart(string label) // 必须与 End 配对;不可嵌套
|
||||
void CollapsingHeaderEnd()
|
||||
```
|
||||
|
||||
> CycleGUI **没有** `BeginChild` / `ChildWindow` / `BeginTabBar` / `BeginTabItem`。容器分区用 `Table(height:)` + `CollapsingHeader`;Tab 切换用 `TabButtons`(见下方"列表与选择")。
|
||||
|
||||
### 按钮与切换
|
||||
|
||||
```csharp
|
||||
// 返回 true 表示被点击
|
||||
bool Button(string text, string shortcut = "", string hint = "", string distinct = "", bool disabled = false)
|
||||
|
||||
// 弹出菜单按钮
|
||||
void PopMenuButton(string buttonTxt, MenuItem[] menu, Action clickBtn = null)
|
||||
|
||||
// 按钮组,返回 true 表示有选择,selecting 为选中索引
|
||||
bool ButtonGroup(string prompt, string[] buttonText, out int selecting, bool sameLine = false)
|
||||
|
||||
// 复选框,返回 true 表示值改变
|
||||
bool CheckBox(string desc, ref bool chk)
|
||||
|
||||
// 开关,返回 true 表示值改变
|
||||
bool Toggle(string desc, ref bool on)
|
||||
|
||||
// 单选按钮,返回 true 表示选择改变
|
||||
bool RadioButtons(string prompt, string[] items, ref int selected, bool sameLine = false)
|
||||
```
|
||||
|
||||
### 输入
|
||||
|
||||
```csharp
|
||||
// 文本输入。ret 为当前文本,doneInput 为 true 表示按下回车
|
||||
(string ret, bool doneInput) TextInput(string prompt, string defaultText = "",
|
||||
string hintText = "", bool focusOnAppearing = false,
|
||||
bool hidePrompt = false, bool alwaysReturnString = false)
|
||||
```
|
||||
|
||||
### 数值控件
|
||||
|
||||
```csharp
|
||||
bool DragFloat(string prompt, ref float valf, float step,
|
||||
float min = float.MinValue, float max = float.MaxValue, bool disableCache = false)
|
||||
|
||||
bool SliderInt(string prompt, ref int val, int min, int max, bool disableCache = false)
|
||||
|
||||
bool SliderFloat(string prompt, ref float val, float min, float max, bool disableCache = false)
|
||||
|
||||
bool DragVector2(string prompt, ref Vector2 vector, float step = 0.01f,
|
||||
float min = float.MinValue, float max = float.MaxValue)
|
||||
|
||||
bool DragMatrix(int m, int n, float[] vals)
|
||||
|
||||
bool BezierEditor(string prompt, ref Vector4 controlPoints, ref float startY, ref float endY)
|
||||
```
|
||||
|
||||
### 颜色
|
||||
|
||||
```csharp
|
||||
bool ColorEdit(string label, ref Color color, bool alphaEnabled = true)
|
||||
```
|
||||
|
||||
### 列表与选择
|
||||
|
||||
```csharp
|
||||
// 返回选中索引,-1 无选择
|
||||
int ListBox(string prompt, string[] items, int height = 5,
|
||||
bool persistentSelecting = false, int preselected = -1)
|
||||
|
||||
bool DropdownBox(string prompt, string[] items, ref int selected)
|
||||
|
||||
// 返回选中标签页索引
|
||||
int TabButtons(string prompt, string[] items, int forceSelect = -1)
|
||||
```
|
||||
|
||||
`forceSelect`:希望激活的标签页索引(0-based);`-1` 不强制。API 内建变化检测:仅当该值相对上次变化时才下发 `ImGuiTabItemFlags_SetSelected`,调用方可每帧传入当前目标 Tab,无需自己做 one-shot 门控。用户手动切换 Tab 后,只要 `forceSelect` 不变就不会再被拉回。
|
||||
|
||||
### 表格
|
||||
|
||||
```csharp
|
||||
unsafe void Table(string strId, string[] header, int rows,
|
||||
Action<Row, int> content, int height = 0,
|
||||
bool enableSearch = false, string title = "",
|
||||
bool freezeFirstCol = false, int scrollToRow = -1,
|
||||
Action<int, bool>? onHeaderSort = null)
|
||||
```
|
||||
|
||||
`height`:可见数据行数;`<=0` 表示不限制高度、显示全部行。`scrollToRow`:希望滚动到可见的数据行索引(0-based);`-1` 不滚动。API 内建变化检测:仅当该值相对上次变化时才下发 `ImGui::SetScrollHereY`,调用方可每帧传入当前选中行,用户之后可自由滚动列表。传入 `-1` 会复位持久态,便于「取消选中后再选同一行」能重新触发滚动。
|
||||
|
||||
Row 对象可用方法:`Label`, `ButtonGroup`, `Checkbox`, `SetColor`, `Image`
|
||||
|
||||
### 图表
|
||||
|
||||
```csharp
|
||||
// 实时折线图,返回可选按钮索引
|
||||
int RealtimePlot(string prompt, float val, bool freeze = false, string[] optionalButtons = null)
|
||||
|
||||
// 迷你指标
|
||||
bool MiniPlot(string name, float value)
|
||||
bool MiniPlot(string name, string value)
|
||||
bool MiniPlot<TEnum>(string name, TEnum value) where TEnum : Enum
|
||||
|
||||
// 静态折线图
|
||||
void Plot2D(string prompt, float[] vals, int height = 200)
|
||||
|
||||
// ⚠️ 以下为占位/空壳(源码 body 为空,调用无效果,请勿依赖):
|
||||
// Progress, BulletText, Indent / UnIndent, QuestionMark, ToolTip
|
||||
// - 需要进度展示:用 RealtimePlot 或 Label 渲染百分比
|
||||
// - 需要 tooltip:用 Button(hint:) / Table.Row.Label(hint:) 等已实现的入口
|
||||
void Progress(float val, float max = 1) // 空壳,无效果
|
||||
```
|
||||
|
||||
### 图像
|
||||
|
||||
```csharp
|
||||
void Image(string prompt, string rgba, int height = -1)
|
||||
|
||||
// 图像列表,返回选中索引
|
||||
int ImageList(string prompt, (string rgba, string top_title, string bottom_title)[] items,
|
||||
bool persistentSelecting = false, int height_px = 100,
|
||||
bool hideSeparator = false, int selecting = -1)
|
||||
```
|
||||
|
||||
### 聊天框
|
||||
|
||||
```csharp
|
||||
(bool ret, string sending) ChatBox(string prompt, string[] appending,
|
||||
int lines = 18, bool input = true)
|
||||
```
|
||||
|
||||
### 文本展示
|
||||
|
||||
```csharp
|
||||
void SelectableText(string prompt, string content, bool copyButton = true)
|
||||
```
|
||||
|
||||
### 文件对话框
|
||||
|
||||
```csharp
|
||||
bool OpenFile(string prompt, string filters, out string dir, string defaultFn = "")
|
||||
bool SaveFile(string prompt, string filter, out string dir, string defaultFn = "")
|
||||
bool SelectFolder(string prompt, out string dir)
|
||||
```
|
||||
|
||||
### Web 与链接
|
||||
|
||||
```csharp
|
||||
void OpenWebview(string name, string url, string hint = "")
|
||||
void DisplayFileLink(string localfilename, string displayName)
|
||||
```
|
||||
|
||||
### 菜单栏
|
||||
|
||||
```csharp
|
||||
void MenuBar(List<MenuItem> menu)
|
||||
```
|
||||
|
||||
MenuItem 构造:
|
||||
```csharp
|
||||
new MenuItem(string label, Action onClick = null, string shortcut = null,
|
||||
bool selected = false, bool enabled = true, List<MenuItem> subItems = null)
|
||||
// label = "-" 表示分隔线
|
||||
```
|
||||
|
||||
### 面板控制
|
||||
|
||||
```csharp
|
||||
bool Closing() // 返回 true 表示用户点击关闭或终端断开
|
||||
void DelegateUI() // 执行通过 Panel.dels 注入的委托 UI
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Panel 配置方法
|
||||
|
||||
维护者核对路径:`D:\MDCS\Source\Core\CycleGUI\CycleGUI\Panel.cs`
|
||||
|
||||
```csharp
|
||||
Panel ShowTitle(string title) // null 隐藏标题栏
|
||||
Panel InitSize(int w = 320, int h = 240)
|
||||
Panel FixSize(int w, int h) // 固定不可调整大小
|
||||
Panel AutoSize(bool set)
|
||||
Panel InitPos(bool pin, int left, int top,
|
||||
float myPivotX = 0, float myPivotY = 0,
|
||||
float screenPivotX = 0, float screenPivotY = 0)
|
||||
Panel InitPosRelative(Panel panel, int left, int top,
|
||||
float relPivotX = 0, float relPivotY = 0,
|
||||
float myPivotX = 0, float myPivotY = 0)
|
||||
Panel SetDefaultDocking(Docking docking, bool auxiliary = false)
|
||||
Panel Modal(bool set)
|
||||
Panel TopMost(bool set)
|
||||
Panel AsToolbarPanel(int height = 38, ToolbarAnchor anchor = ToolbarAnchor.RightTop)
|
||||
```
|
||||
|
||||
枚举:
|
||||
- `Panel.Docking`:`Left`, `Top`, `Right`, `Bottom`, `None`, `Full`
|
||||
- `Panel.ToolbarAnchor`:`RightTop`, `LeftBottom`
|
||||
|
||||
实例方法:
|
||||
```csharp
|
||||
void Define(CycleGUIHandler handler)
|
||||
void Repaint(bool dropCurrent = false, int repaintTimeMs = 30)
|
||||
// dropCurrent=true:丢弃当前帧,立即重绘(编辑后同步 UI 时用)
|
||||
// repaintTimeMs:持续 Repaint 时的间隔,默认 30ms
|
||||
// 只在有实时数据的面板持续调用;静态面板不要无条件 Repaint
|
||||
void BringToFront()
|
||||
void Exit()
|
||||
void Freeze() / void UnFreeze()
|
||||
void SwitchTerminal(Terminal newTerminal)
|
||||
bool Probe<T>(CycleGUIProber<T> prober, out T val)
|
||||
void FireAndForget(...)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## HoverMenu
|
||||
|
||||
维护者核对路径:`D:\MDCS\Source\Core\CycleGUI\CycleGUI\API\HoverMenu.cs`
|
||||
|
||||
```csharp
|
||||
HoverMenu GUI.DeclareHoverMenu()
|
||||
|
||||
enum HoverMenuPlacement {
|
||||
Cursor = 0,
|
||||
ObjectScreenAnchor = 1, // 默认;使用 hovered object 的屏幕投影锚点
|
||||
ObjectBounds = 2, // 预留,v1 不支持
|
||||
}
|
||||
|
||||
class HoverMenu : IDisposable {
|
||||
string Name { get; set; }
|
||||
string TargetObjectName { get; set; }
|
||||
HoverMenuPlacement Placement { get; set; } // 默认 ObjectScreenAnchor
|
||||
int OffsetX { get; set; } // 默认 12
|
||||
int OffsetY { get; set; } // 默认 12
|
||||
int CloseDelayMs { get; set; } // 默认 180
|
||||
PanelBuilder.CycleGUIHandler Build { get; set; }
|
||||
Terminal Terminal { get; }
|
||||
void Start()
|
||||
void StartOnTerminal(Terminal terminal)
|
||||
void Stop()
|
||||
void Dispose()
|
||||
}
|
||||
```
|
||||
|
||||
实现语义:每个 terminal 一个 manager/native listener,同一时间复用一个 transient panel;C# 侧按 `TargetObjectName` 字典匹配,暂假设一个 object 一个 HoverMenu,重复注册同一 object 会抛异常。native feedback 按 hovered object、object anchor、dragging 去重,不按鼠标移动持续推送。
|
||||
|
||||
---
|
||||
|
||||
## Workspace Props 完整列表
|
||||
|
||||
维护者核对路径:`D:\MDCS\Source\Core\CycleGUI\CycleGUI\API\Workspace.Props.cs`
|
||||
|
||||
### 模型
|
||||
|
||||
```csharp
|
||||
// 加载 GLTF 模型类(不可 Remove)
|
||||
LoadModel { name, detail: ModelDetail }
|
||||
|
||||
// ModelDetail 结构
|
||||
ModelDetail(byte[] gltfBytes) {
|
||||
Center = Vector3,
|
||||
Rotate = Quaternion,
|
||||
Scale = float,
|
||||
ColorBias = Vector3,
|
||||
ColorScale = float,
|
||||
Brightness = float,
|
||||
ForceDblFace = bool,
|
||||
NormalShading = float,
|
||||
}
|
||||
|
||||
// 更新已加载模型的参数(不重新传 GLTF 数据)
|
||||
ReloadModel { name, detail: ModelDetail }
|
||||
|
||||
// 放置模型实例
|
||||
PutModelObject { name, clsName, newPosition: Vector3, newQuaternion: Quaternion, baseColor?: Color /* 预留,引擎当前忽略 */ }
|
||||
|
||||
// 自定义网格
|
||||
DefineMesh { clsname, positions: float[] /* 三角面顶点,长度必须是9的倍数 */, color: Color, smooth: bool }
|
||||
```
|
||||
|
||||
### 点云
|
||||
|
||||
```csharp
|
||||
PutPointCloud {
|
||||
name: string,
|
||||
xyzSzs: Vector4[], // xyz + size
|
||||
colors: Color[], // 每点一个 System.Drawing.Color
|
||||
newPosition: Vector3,
|
||||
newQuaternion: Quaternion,
|
||||
handleString: string, // 空=无句柄, 单字符=字符句柄, 多字符=PNG图标
|
||||
type: int, // 0=普通, 1=SLAM地图
|
||||
}
|
||||
```
|
||||
|
||||
### 线和曲线
|
||||
|
||||
```csharp
|
||||
PutStraightLine {
|
||||
name, start: Vector3, end: Vector3,
|
||||
propStart: string, propEnd: string, // 绑定到对象(空则用 start/end)
|
||||
arrowType: Painter.ArrowType, width: int, color: Color, dashDensity: int,
|
||||
}
|
||||
|
||||
PutBezierCurve {
|
||||
name, start: Vector3, end: Vector3,
|
||||
control1: Vector3, control2: Vector3,
|
||||
width: int, color: Color,
|
||||
}
|
||||
|
||||
PutVector { name, from: Vector3, dir: Vector3, color: Color, length: int }
|
||||
```
|
||||
|
||||
### 图像与纹理
|
||||
|
||||
```csharp
|
||||
// 静态图像
|
||||
PutImage { name, rgbaName, displayType, displayH, displayW, /* transform fields */ }
|
||||
|
||||
// 可更新 RGBA 纹理
|
||||
PutRGBA { name, width, height, requestRGBA / rgba }
|
||||
.StartStreaming() -> Action<byte[]> // 返回更新函数
|
||||
.Invalidate()
|
||||
.UpdateRGBA(byte[])
|
||||
|
||||
// SVG
|
||||
DeclareSVG { name, svgContent: string }
|
||||
```
|
||||
|
||||
### 对象操作
|
||||
|
||||
```csharp
|
||||
// 变换对象位置/旋转
|
||||
TransformObject { name, pos: Vector3, quat: Quaternion, timeMs: int, coord: Coord }
|
||||
// Coord: Absolute / Relative
|
||||
|
||||
// 变换子对象
|
||||
TransformSubObject { objectNamePattern, subObjectName/subObjectId, translation, rotation, timeMs }
|
||||
|
||||
// 对象锚定(moon 跟随 earth)
|
||||
SetObjectMoonTo { name: "moon", earth: "earth_obj", pos: Vector3, quat: Quaternion }
|
||||
|
||||
// 删除对象(通配符)
|
||||
WorkspaceProp.RemoveNamePattern(string namePattern) // 支持 * 和 ?
|
||||
```
|
||||
|
||||
### 文本与图标
|
||||
|
||||
```csharp
|
||||
PutHandleIcon { name, ... }
|
||||
PutTextAlongLine { name, ... }
|
||||
SpotText { /* fluent builder */ }
|
||||
```
|
||||
|
||||
### 高斯溅射(Gaussian Splatting)
|
||||
|
||||
```csharp
|
||||
PutGaussianSplats {
|
||||
name, splats: GaussianSplat[],
|
||||
globalOpacityScale, globalSizeScale, renderMode, ...
|
||||
}
|
||||
PutGaussianSplats.FromPLY(path, name, axisPreset)
|
||||
PutGaussianSplats.FromPointCloud(...)
|
||||
PutGaussianSplats4D { /* 4D 时序数据 */ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Workspace UI 操作
|
||||
|
||||
维护者核对路径:`D:\MDCS\Source\Core\CycleGUI\CycleGUI\API\Workspace.UIOps.cs`
|
||||
|
||||
### 相机
|
||||
|
||||
```csharp
|
||||
SetCamera {
|
||||
lookAt: Vector3,
|
||||
azimuth: float, // 默认 -PI/2
|
||||
altitude: float, // 默认 PI/2
|
||||
distance: float,
|
||||
fov: float,
|
||||
projectionMode: Perspective / Orthographic,
|
||||
displayMode: Normal / VR / EyeTrackedHolography / EyeTrackedLenticular,
|
||||
anchor_type: Both / StareOnly / PositionOnly / CopyCamera,
|
||||
azimuth_range: Vector2, // 限制旋转范围
|
||||
altitude_range: Vector2,
|
||||
pan_range: Vector3, // 限制平移范围
|
||||
mmb_freelook: bool, // 中键自由视角
|
||||
}
|
||||
// 提交方式:.IssueToDefault() / .IssueToAllTerminals() / .IssueToTerminal(t)
|
||||
```
|
||||
|
||||
### 外观
|
||||
|
||||
```csharp
|
||||
SetAppearance {
|
||||
useEDL: bool, // Eye-Dome Lighting
|
||||
useSSAO: bool, // 环境光遮蔽
|
||||
useGround: bool, // 地面
|
||||
useBorder: bool, // 边框
|
||||
useBloom: bool, // 泛光
|
||||
drawGroundGrid: bool, // 地面网格
|
||||
drawGuizmo: bool, // 坐标轴指示器
|
||||
useDefaultSky: bool, // 默认天空盒
|
||||
sun_altitude: float, // 太阳高度
|
||||
hover_shine: Color, // 悬停高亮色
|
||||
selected_shine: Color, // 选中高亮色
|
||||
hover_border_color: Color,
|
||||
selected_border_color: Color,
|
||||
world_border_color: Color,
|
||||
voxel_quantize: float, // 体素量化
|
||||
voxel_opacity: float, // 体素不透明度
|
||||
clippingPlanes: (Vector3 center, Vector3 direction)[], // 最多4个裁剪面
|
||||
bring2front_onhovering: bool,
|
||||
}
|
||||
```
|
||||
|
||||
### 对象外观
|
||||
|
||||
```csharp
|
||||
SetObjectApperance { namePattern, bring_to_front, shine_color: Color, use_border, transparency }
|
||||
SetModelObjectProperty { namePattern, baseAnimId, nextAnimId, material_variant, team_color: Color,
|
||||
base_stopatend, next_stopatend, animate_asap }
|
||||
SetPropShowHide { namePattern, show: bool } // sticky pattern (2026-05+)
|
||||
SetPropApplyCrossSection { namePattern, apply: bool } // sticky pattern (2026-05+)
|
||||
```
|
||||
|
||||
> **Sticky pattern 语义**(适用于 `SetPropShowHide` / `SetPropApplyCrossSection` /
|
||||
> `SelectObject.SetObjectSelectable` / `SelectObject.SetObjectSubSelectable`):每次调用先按
|
||||
> pattern 扫一遍 `global_name_map` 立即生效,**同时**把 pattern 字符串记入当前 viewport 的
|
||||
> `workspace_state.*_patterns`。之后 `Workspace.AddProp` 加入的新对象会被引擎在 `indexier::add`
|
||||
> 自动按 memo 重匹配并设位/隐藏/纳入 selectables。要取消粘性,用**同一 pattern 字符串**反向调
|
||||
> 一次(`SetPropShowHide{show=true}` / `SetObjectUnselectable(pattern)` 等)。
|
||||
>
|
||||
> **Pattern 是 glob**(`*` / `?`),引擎全栈唯一语义。底层是 `libVRender::wildcardMatch`,
|
||||
> 2026-05 统一化后已删除 `RegexMatcher::match` / `regexMatch` 全套 regex 路径。详见
|
||||
> SKILL.md 的 `## namePattern 语法 = glob` 章节,那里有完整的 API 列表 + 历史根因 +
|
||||
> "真要 regex 怎么办" 指引。
|
||||
|
||||
### 交互操作
|
||||
|
||||
所有交互操作继承 `WorkspaceUIOperation<T>`,通过 `.Start()` 启动,`.End()` 结束,`.feedback` 回调接收结果。
|
||||
|
||||
```csharp
|
||||
// 拾取世界坐标
|
||||
GetPosition {
|
||||
method: PickMode.GridPlane / Holo3D,
|
||||
snaps: string[], // 可吸附对象名
|
||||
}
|
||||
// feedback: Action<WorldPosition, op>
|
||||
// WorldPosition { mouse_pos, snapping_object, object_pos, sub_id }
|
||||
|
||||
// 拖拽跟随
|
||||
FollowMouse {
|
||||
method: FollowingMethod.LineOnGrid / RectOnGrid / PointOnGrid / CircleOnGrid / Line3D / ...,
|
||||
follower_objects: string[],
|
||||
start_snapping_objects: string[],
|
||||
end_snapping_objects: string[],
|
||||
realtime: bool,
|
||||
}
|
||||
// feedback: Action<FollowFeedback, op>
|
||||
|
||||
// 对象选择
|
||||
SelectObject {
|
||||
fineSelectOnPointClouds: bool,
|
||||
fineSelectOnHandle: bool,
|
||||
}
|
||||
// feedback: Action<(string name, BitArray selector, string firstSub)[], op>
|
||||
// feedback 返回的是“当前完整选中集”(不是 delta),由引擎处理 Shift+加选 / Alt+减选 / 框选
|
||||
// 运行时切换模式:selectOp.SetSelectionMode(SelectionMode.Rectangle, paintRadius)
|
||||
// SelectionMode: Click / Rectangle / Paint
|
||||
//
|
||||
// SetObjectSelectable(pattern) / SetObjectSubSelectable(pattern)
|
||||
// - pattern 是 glob:"*" = 任意子串、"?" = 任意单字符;不是 ECMAScript 正则
|
||||
// - 立即对已存在对象按 glob 设可选位
|
||||
// - 并把 pattern 记入 workspace_state.selectable_patterns(sticky,2026-05 起)
|
||||
// - 之后 AddProp 加入的新对象会被引擎自动匹配 + 设位(indexier::add hook)
|
||||
// - 取消 sticky:用同一 pattern 调 SetObjectUnselectable / SetObjectSubUnselectable
|
||||
|
||||
// Guizmo 操作(移动/旋转 gizmo)
|
||||
GuizmoAction {
|
||||
dof: GuizmoDof, // 默认 TranslateXYZ
|
||||
realtimeResult: bool,
|
||||
}
|
||||
// GuizmoDof: None, TranslateX/Y/Z, RotateX/Y/Z, Roll, Pitch, Yaw,
|
||||
// TranslateXYZ, RotateXYZ, PlanarXY, PlanarXYYaw, All
|
||||
// feedback: Action<(string name, Vector3 pos, Quaternion rot)[], op>
|
||||
|
||||
// 原始鼠标事件
|
||||
RegisterMouseAction {
|
||||
listen_MouseDown, listen_MouseUp, listen_MouseMove, listen_Wheel: bool,
|
||||
}
|
||||
// feedback: Action<MouseAction, op>
|
||||
// MouseAction { mouseX, mouseY, workspaceX, workspaceY, mouseLB/RB/MB, wheelDelta }
|
||||
```
|
||||
|
||||
### 工作区行为
|
||||
|
||||
```csharp
|
||||
SetWorkspaceBehaviour {
|
||||
operation_trigger: Mouse.MouseLB, // 操作触发键
|
||||
workspace_pan: Mouse.MouseRB, // 平移键
|
||||
workspace_orbit: Mouse.MouseMB, // 旋转键
|
||||
}
|
||||
// Mouse: MouseLB, MouseMB, MouseRB, CtrlMouseLB, CtrlMouseMB, CtrlMouseRB
|
||||
```
|
||||
|
||||
### 查询操作
|
||||
|
||||
```csharp
|
||||
// 查询视口相机状态
|
||||
new QueryViewportState { callback = state => {
|
||||
// state.CameraPosition, state.LookAt, state.Up
|
||||
}}.IssueToDefault();
|
||||
|
||||
// 捕获渲染画面
|
||||
new CaptureRenderedViewport { callback = img => {
|
||||
// img.bytes (RGB), img.width, img.height
|
||||
}}.IssueToDefault();
|
||||
|
||||
// 查询图形信息
|
||||
new QueryGraphics { callback = state => {
|
||||
// state.MonitorCount, state.Monitors[], state.FPS
|
||||
}}.IssueToDefault();
|
||||
|
||||
// 聚焦到对象
|
||||
new FrameToFit { name = "objectName", margin = 1.1f }.IssueToDefault();
|
||||
|
||||
// 全屏
|
||||
new SetFullScreen { fullscreen = true, screen_id = -1 }.IssueToDefault();
|
||||
|
||||
// 自定义网格
|
||||
new SetOperatingGridAppearance {
|
||||
show = true, pivot = Vector3.Zero,
|
||||
unitX = Vector3.UnitX, unitY = Vector3.UnitY,
|
||||
}.IssueToDefault();
|
||||
|
||||
// ImGui 样式
|
||||
new SetImGUIStyle {
|
||||
windowBg = 0xFF171717, button = 0xFF3D3835,
|
||||
windowRounding = 6f, frameRounding = 6f,
|
||||
}.IssueToDefault();
|
||||
|
||||
// 主窗口菜单栏
|
||||
new SetMainMenuBar {
|
||||
menu = new List<MenuItem> { ... }, show = true
|
||||
}.IssueToDefault();
|
||||
|
||||
// Viewport 专属菜单栏
|
||||
var viewport = GUI.PromptWorkspaceViewport();
|
||||
new SetWorkspaceMenuBar {
|
||||
viewport = viewport,
|
||||
menu = new List<MenuItem> { ... }, show = true
|
||||
}.Issue();
|
||||
|
||||
// 面板持久菜单栏(从 handler 外部设置,自动注入到每次重绘)
|
||||
new SetPanelMenuBar {
|
||||
panel = myPanel,
|
||||
menu = new List<MenuItem> { ... }
|
||||
}.Issue();
|
||||
// 清除:new SetPanelMenuBar { panel = myPanel, menu = null }.Issue();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Painter 完整 API
|
||||
|
||||
维护者核对路径:`D:\MDCS\Source\Core\CycleGUI\CycleGUI\Painter.cs`
|
||||
|
||||
```csharp
|
||||
// 获取/创建命名 Painter(单例)
|
||||
static Painter GetPainter(string name)
|
||||
|
||||
// 清除当前帧数据(双缓冲交换)
|
||||
void Clear()
|
||||
|
||||
// 绘制点(单位:米)
|
||||
void DrawDot(Color color, Vector3 xyz, float size = 1f)
|
||||
|
||||
// 绘制线段
|
||||
void DrawLine(Color color, Vector3 start, Vector3 end,
|
||||
float width = 1f, ArrowType arrow = ArrowType.None, int dashScale = 0)
|
||||
// ArrowType: None, Start, End
|
||||
|
||||
// 绘制方向向量(像素长度)
|
||||
void DrawVector(Vector3 from, Vector3 dir, Color color,
|
||||
float width = 1f, int pixels = 10)
|
||||
|
||||
// 绘制 3D 文本
|
||||
void DrawText(Color color, Vector3 tCenter, string s)
|
||||
|
||||
// 绘制区域体素
|
||||
void DrawRegion3D(Color color, Vector3 center)
|
||||
|
||||
// 绘制填充多边形(2D 点 + 3D 变换)
|
||||
void DrawPolygonFilled(Vector2[] points2D, Vector3 trans, Quaternion quat,
|
||||
Color borderColor, Color fillColor, float thickness = 0f)
|
||||
// thickness=0: 平面多边形; >0: 拉伸立体
|
||||
|
||||
// 终端隔离
|
||||
Painter.terminal = specificTerminal; // null 则广播到所有终端
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## LeastServer(HTTP/WebSocket 服务)
|
||||
|
||||
全局类(无命名空间),维护者核对路径:`D:\MDCS\Source\Core\CycleGUI\CycleGUI\Terminals\LeastServer.cs`
|
||||
|
||||
```csharp
|
||||
static void Listener(int port) // 阻塞式启动监听
|
||||
static void AddGetHandler(string path, Func<string> handler)
|
||||
static void AddPostTextHandler(string path, Func<string, string> handler)
|
||||
static void AddPostByteHandler(string path, Func<byte[], byte[]> handler)
|
||||
static void AddServingFiles(string urlPrefix, string localPath)
|
||||
static void AddServingResources(string urlPrefix, Assembly, string ns)
|
||||
static void AddSpecialTreat(string path, Action<...> handler) // WebSocket 升级
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Viewport(面板内嵌 3D 视口)
|
||||
|
||||
```csharp
|
||||
// 创建内嵌 Workspace 视口面板
|
||||
var viewport = GUI.PromptWorkspaceViewport();
|
||||
```
|
||||
|
||||
`Viewport` 继承 `Panel`,拥有独立的 `ViewportSubTerminal`,Workspace 状态与主终端隔离。
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -0,0 +1,310 @@
|
||||
# 面板、控件、工具栏和菜单栏
|
||||
|
||||
## Panel 创建方式
|
||||
|
||||
- `GUI.PromptPanel(handler)`:立即显示面板。
|
||||
- `GUI.DeclarePanel()`:先创建 `Panel`,后续 `Define(handler)` 绑定内容。
|
||||
- `GUI.PromptAndWaitPanel(handler)`:阻塞式模态面板。
|
||||
- `GUI.PromptOrBringToFront(handler, instancingObject: key)`:单实例面板。
|
||||
|
||||
```csharp
|
||||
var panel = GUI.DeclarePanel()
|
||||
.ShowTitle("Settings")
|
||||
.InitSize(400, 300)
|
||||
.InitPos(false, 100, 100)
|
||||
.SetDefaultDocking(Panel.Docking.Left);
|
||||
|
||||
panel.Define(pb =>
|
||||
{
|
||||
pb.Label("Ready");
|
||||
if (pb.Closing()) panel.Exit();
|
||||
});
|
||||
```
|
||||
|
||||
## Panel 配置
|
||||
|
||||
- `ShowTitle(null)` 隐藏标题栏;传字符串设置标题。
|
||||
- `InitSize(w, h)` 设置初始大小。
|
||||
- `InitPos(pin, left, top, myPivotX, myPivotY, screenPivotX, screenPivotY)` 设置屏幕相对位置。
|
||||
- `InitPosRelative(panel, left, top, relPivotX, relPivotY, myPivotX, myPivotY)` 设置相对位置。
|
||||
- `SetDefaultDocking(Panel.Docking.Left/Top/Right/Bottom/None/Full)` 设置默认停靠。
|
||||
- `AutoSize(true)`、`Modal(true)`、`TopMost(true)` 分别控制自适应大小、模态、置顶。
|
||||
|
||||
`InitPosRelative` 没有 `offsetX` / `offsetY` 命名参数。
|
||||
|
||||
## Panel 生命周期
|
||||
|
||||
- `panel.Repaint()` / `pb.Panel.Repaint()`:请求重绘;handler 会在约 `repaintTimeMs`(默认 30ms)后再次执行。
|
||||
- `panel.Repaint(true)`:丢弃当前帧并立即重绘(数据刚被用户编辑、需要同步其它面板时用)。
|
||||
- `panel.Repaint()` 也可从其它线程调用,用于后台数据推送。
|
||||
- `panel.BringToFront()`:置前。
|
||||
- `panel.Exit()`:关闭。
|
||||
- `panel.Freeze()` / `panel.UnFreeze()`:冻结/解冻。
|
||||
- `pb.Closing()`:用户点击关闭时返回 true,通常随后调用 `panel.Exit()`。
|
||||
|
||||
### 立即模式与 Repaint 策略(重要)
|
||||
|
||||
CycleGUI 是立即模式:**handler 只在「被请求重绘」时才重新执行**。若 handler 末尾没有 `Repaint()`,面板通常只在控件返回 true(用户点击、输入等)那一帧重绘。
|
||||
|
||||
#### 默认原则:不要无条件 Repaint
|
||||
|
||||
**不要在每个面板 handler 末尾无条件写 `pb.Panel.Repaint()`**,除非该面板确实有实时变化的数据。静态配置页、Actions 按钮组、Properties 表格等在无交互时不应持续刷新——否则 handler 会以 ~30ms 间隔空转,浪费 CPU。
|
||||
|
||||
| 面板内容 | 是否持续 Repaint |
|
||||
|---------|-----------------|
|
||||
| 静态 Properties / Actions / 配置表单 | 否,交互时自动重绘 |
|
||||
| Status 字段、实时指标、MiniPlot / RealtimePlot | 是 |
|
||||
| 后台线程推送的数据(遥测、诊断日志等) | 是(线程侧或对应 Tab 内 Repaint) |
|
||||
| 延迟回调改完 ListBox / Table 数据 | 仅回调内 Repaint 一次 |
|
||||
|
||||
#### 何时必须显式 Repaint
|
||||
|
||||
1. **延迟回调改数据**:`PopMenuButton` / `MenuItem`、`UITools.Input` 等对话框确认回调发生时不在正常渲染帧内,`ListBox` / `Table` 不会自动反映新数据。
|
||||
|
||||
```csharp
|
||||
pb.PopMenuButton("Add", types.Select(t => new MenuItem(t.Name, () =>
|
||||
{
|
||||
if (UITools.Input("Name", def, out var name, ...))
|
||||
{
|
||||
list.Add(Create(t, name));
|
||||
pb.Panel.Repaint(); // 否则上面的 ListBox 不刷新
|
||||
}
|
||||
})).ToArray());
|
||||
var sel = pb.ListBox("Existing", list.Select(x => x.name).ToArray());
|
||||
```
|
||||
|
||||
2. **用户编辑后需同步其它面板**:`pb.Panel.Repaint(true)`,并可 `otherPanel?.Repaint()`。
|
||||
|
||||
3. **后台线程更新 UI 数据**:在更新完共享数据后 `panel.Repaint()`(见 `examples.md` 实时图表示例)。
|
||||
|
||||
#### 复合面板:按 Tab / 区块决定是否 Repaint
|
||||
|
||||
多 Tab 面板(`TabButtons`)应只在**当前 Tab 或区块有实时数据**时 Repaint,而不是在 handler 末尾一刀切:
|
||||
|
||||
```csharp
|
||||
var tab = pb.TabButtons("edit", new[] { "Properties", "Status", "Actions" });
|
||||
if (tab == 0)
|
||||
{
|
||||
// 静态配置,交互时控件自身触发重绘,无需 Repaint
|
||||
}
|
||||
else if (tab == 1)
|
||||
{
|
||||
pb.Table("Status", headers, rows, (row, i) => { row.Label(liveValues[i]); });
|
||||
pb.Panel.Repaint(); // Status 实时刷新
|
||||
}
|
||||
else if (tab == 2)
|
||||
{
|
||||
if (pb.ButtonGroup("Actions", actionNames, out var sel))
|
||||
actions[sel]();
|
||||
// Actions 静态按钮,不 Repaint
|
||||
}
|
||||
```
|
||||
|
||||
若 handler 上半部还有**独立的实时区块**(例如 Locator 的 `DrawUI` 显示 `Status` / `Working`),下半部 Tab 可能是静态的——用标志位或虚属性标记「上半部需要 live 刷新」,仅在该标志为 true 时 Repaint:
|
||||
|
||||
```csharp
|
||||
inst.DrawUI(pb); // 子类可 override DrawUILive => true
|
||||
ShowPSA(pb, ...); // Status Tab 内部自行 Repaint
|
||||
if (inst.DrawUILive)
|
||||
pb.Panel.Repaint();
|
||||
```
|
||||
|
||||
#### 典型实时面板
|
||||
|
||||
状态栏、诊断条、带 `MiniPlot` / `RealtimePlot` 的监控面板——handler 末尾 **可以** 无条件 `pb.Panel.Repaint()`,因为内容每帧都在变。
|
||||
|
||||
```csharp
|
||||
return pb =>
|
||||
{
|
||||
pb.Label($"Pos: {state.x:F1}, {state.y:F1}");
|
||||
pb.MiniPlot("Loop ms", state.loopMs);
|
||||
pb.Panel.Repaint();
|
||||
};
|
||||
```
|
||||
|
||||
## PanelBuilder 常用控件
|
||||
|
||||
```csharp
|
||||
pb.Label("Status");
|
||||
pb.Separator();
|
||||
pb.SeparatorText("Section");
|
||||
pb.SameLine();
|
||||
|
||||
if (pb.Button("Click", hint: "tooltip")) { }
|
||||
var (text, done) = pb.TextInput("Name", defaultText: "hello", hintText: "placeholder");
|
||||
pb.CheckBox("Enable", ref enabled);
|
||||
pb.Toggle("Dark Mode", ref darkMode);
|
||||
pb.RadioButtons("Choice", new[] { "A", "B" }, ref selected);
|
||||
pb.DropdownBox("Type", new[] { "X", "Y" }, ref selectedIdx);
|
||||
pb.SliderFloat("Volume", ref volume, 0, 1);
|
||||
pb.SliderInt("Count", ref count, 0, 100);
|
||||
pb.DragFloat("Speed", ref speed, 0.1f, 0, 10);
|
||||
pb.DragVector2("Position", ref pos, 0.01f);
|
||||
pb.ColorEdit("Color", ref color);
|
||||
```
|
||||
|
||||
布局约束:没有 `BeginChild` / `ChildWindow` / `BeginTabBar` / `BeginTabItem`。分区列表用 `Table(..., height: N)`,折叠区域用 `CollapsingHeaderStart/End`,Tab 切换用 `TabButtons`。
|
||||
|
||||
空壳控件不要依赖:`Progress`、`BulletText`、`Indent` / `UnIndent`、`QuestionMark`、`ToolTip`。进度用 `RealtimePlot` 或 `Label`,tooltip 用 `Button(hint:)` 等入口。
|
||||
|
||||
## 控件 id 与 Duplicated id(重要)
|
||||
|
||||
CycleGUI 给每个**带状态的控件**用标签算一个 id(`ImHashStr`)。同一面板(同一次 handler 执行)内出现两个**相同 id** 就会抛异常并使该面板崩溃:
|
||||
|
||||
```
|
||||
(Exception): Duplicated id `终点筛选(站点ID或名称)`
|
||||
at CycleGUI.PanelBuilder.TextInput(...)
|
||||
```
|
||||
|
||||
### 根因:id 用 ASCII 算哈希
|
||||
|
||||
`ImHashStr` 内部是 `Encoding.ASCII.GetBytes(label)` 再做 CRC32。**所有非 ASCII 字符(中文、全角括号()【】、全角空格等)都会被折叠成 `?`**,于是不同的中文可能折叠出相同的字节序列:
|
||||
|
||||
- 两个**同字数的纯中文**标签 → 折叠后完全相同 → 同 id → 崩。
|
||||
例:`筛选条件`(TextInput) 与 `设为类型`(DropdownBox) 都是 4 个汉字 → 都折叠成 `????`。
|
||||
- 两个仅中文字符不同、ASCII 部分相同的标签 → 同样相撞。
|
||||
例:`起点筛选(站点ID或名称)` 与 `终点筛选(站点ID或名称)` → 折叠后只剩 `ID` 等 ASCII 一致 → 相撞。
|
||||
|
||||
### 哪些控件受影响
|
||||
|
||||
凡是用「第一个标签参数」算 id 的都受影响:`TextInput` / `DropdownBox` / `CheckBox` / `Toggle` / `ButtonGroup` / `ListBox` / `RadioButtons` / `Table` / `TabButtons` / `ColorEdit` / `SliderInt` / `SliderFloat` / `DragFloat` / `DragVector2` / `RealtimePlot` / `ChatBox` / `CollapsingHeaderStart` / `MiniPlot` 等;`Button` 的 id = `text + "-" + distinct`。
|
||||
|
||||
不受影响:`Label` / `SeparatorText` / `Separator`(无 id);`SelectableText` 用 `content`(不是 prompt)算 id。
|
||||
|
||||
### 修法(三选一,优先 ①)
|
||||
|
||||
① **加 `###` 唯一后缀**:`ImHashStr` 实现了 ImGui 的 `###` 约定——遇到 `###` 会重置哈希,只用 `###` 之后的 ASCII 算 id;显示文字仍是 `###` 之前的部分。后缀必须**纯 ASCII** 且在本面板内唯一。
|
||||
|
||||
```csharp
|
||||
// 错:两个 4 字纯中文标签 → Duplicated id
|
||||
pb.TextInput("筛选条件", ...);
|
||||
pb.DropdownBox("设为类型", ...);
|
||||
|
||||
// 对:显示不变,id 唯一
|
||||
pb.TextInput("筛选条件###flt-keyword", ...);
|
||||
pb.DropdownBox("设为类型###set-kind", ...);
|
||||
```
|
||||
|
||||
② **按钮用唯一 `distinct:`**:给每个 `Button` 一个唯一的纯 ASCII `distinct`,即使文案是纯中文也不撞。
|
||||
|
||||
```csharp
|
||||
if (pb.Button("保存", distinct: "cfg-save")) { }
|
||||
if (pb.Button("删除", distinct: "cfg-del")) { }
|
||||
```
|
||||
|
||||
③ **加 ASCII 前缀**:表单字段统一用 `1. ` / `2. ` / `3. ` 等数字前缀(数字是 ASCII,天然区分),既排版整齐又避免相撞。
|
||||
|
||||
```csharp
|
||||
pb.TextInput("1. 名称", ...);
|
||||
pb.TextInput("2. 备注", ...);
|
||||
```
|
||||
|
||||
### 作用域与循环
|
||||
|
||||
- id 唯一性是**「每面板每帧」**判定:不同面板之间不会相撞(panelId 已混入哈希种子),只需保证同一 handler 内不重复。
|
||||
- **循环里渲染同一标签的带状态控件**也会自撞(第二次迭代即重复 id)。给每项拼上唯一 ASCII(索引或主键):
|
||||
|
||||
```csharp
|
||||
foreach (var item in items)
|
||||
pb.CheckBox($"启用###en-{item.Id}", ref item.Enabled); // 不要写死 "启用"
|
||||
```
|
||||
|
||||
> 表格行内的 `row.Label` / `row.ButtonGroup` / `row.Checkbox` 由 `Table` 在原生侧按单元格分配 id,不走 `ImHashStr`,不受此限制。
|
||||
|
||||
## 表格和标签页
|
||||
|
||||
```csharp
|
||||
pb.Table("data", new[] { "Name", "Value", "Action" }, rowCount, (row, i) =>
|
||||
{
|
||||
row.Label(names[i]);
|
||||
row.Label(values[i].ToString());
|
||||
if (row.ButtonGroup("", new[] { "Edit", "Del" }, out int act)) { }
|
||||
}, height: 12, enableSearch: true);
|
||||
|
||||
int tab = pb.TabButtons("Tabs", new[] { "General", "Advanced" });
|
||||
```
|
||||
|
||||
### 程序化导航(选中同步 Tab / 列表滚动)
|
||||
|
||||
`TabButtons` 与 `Table` 支持声明式导航参数,**变化检测在 API 内部完成**,调用方每帧传入当前目标即可:
|
||||
|
||||
```csharp
|
||||
// 根据业务选中态每帧推导目标 Tab;仅当目标 Tab 变化时才自动切换
|
||||
var nav = pb.TabButtons("cmp-nav", navLabels, forceSelect: (int)targetNavKind);
|
||||
|
||||
// 根据当前选中对象在 rows 中的索引每帧传入;仅当索引变化时才滚动列表
|
||||
var scrollRow = FindSelectedRowIndex(rows);
|
||||
pb.Table("cmp-scn-all", headers, rows.Count, (row, i) => { ... },
|
||||
height: visibleRows, scrollToRow: scrollRow);
|
||||
```
|
||||
|
||||
- `forceSelect` / `scrollToRow` 传 `-1` 表示无目标;会复位 API 内部持久态,便于「取消选中后再选同一项」重新触发。
|
||||
- 用户手动切 Tab 或滚动列表后,只要 `forceSelect` / `scrollToRow` 不变,就不会被每帧拉回。
|
||||
- 面板置顶仍用 `panel.BringToFront()`(一次性,由应用在选中变更时自行触发)。
|
||||
|
||||
## 工具栏
|
||||
|
||||
```csharp
|
||||
var toolbar = GUI.DeclarePanel()
|
||||
.ShowTitle(null)
|
||||
.AsToolbarPanel(40, Panel.ToolbarAnchor.RightTop);
|
||||
|
||||
toolbar.Define(pb =>
|
||||
{
|
||||
var tpb = new ToolbarPanelBuilder(pb);
|
||||
tpb.Label("Tools");
|
||||
tpb.Separator();
|
||||
if (tpb.Button("Run", distinct: "run")) { }
|
||||
tpb.PopMenuButton("More", new[]
|
||||
{
|
||||
new MenuItem("Option A", () => { }),
|
||||
new MenuItem("-"),
|
||||
new MenuItem("Option B", () => { }),
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
规则:高度建议 `>= 40`;工具栏无自动换行/横向滚动;项目过多时放进 `PopMenuButton`;要让 toolbar 避让旁边面板,旁边面板必须真的 dock 进 dockspace。
|
||||
|
||||
## HoverMenu
|
||||
|
||||
`HoverMenu` 用于 3D Workspace 对象 hover 后出现的可点击浮窗。它不是 tooltip:默认按对象屏幕锚点定位,鼠标移向浮窗按钮时不会跟随鼠标逃逸。
|
||||
|
||||
```csharp
|
||||
var menu = GUI.DeclareHoverMenu();
|
||||
menu.TargetObjectName = "site_a";
|
||||
menu.OffsetX = 14;
|
||||
menu.OffsetY = 14;
|
||||
menu.CloseDelayMs = 180;
|
||||
menu.Build = pb =>
|
||||
{
|
||||
pb.Label("Site A");
|
||||
pb.Separator();
|
||||
if (pb.Button("Move", distinct: "site_a")) { }
|
||||
if (pb.Button("Properties", distinct: "site_a")) { }
|
||||
};
|
||||
menu.StartOnTerminal(terminal);
|
||||
```
|
||||
|
||||
规则:一个 object 暂只注册一个 HoverMenu;manager 内部按 `TargetObjectName` 建字典索引,几千个对象时匹配不是线性扫描。`Placement` 默认 `ObjectScreenAnchor`;`Cursor` 只适合一次性定位类场景,不要作为可点击菜单默认模式;`ObjectBounds` 预留,暂不实现。关闭菜单或移除对象时调用 `menu.Stop()` / `Dispose()`。
|
||||
|
||||
## 菜单栏
|
||||
|
||||
- `pb.MenuBar(...)`:handler 内的一次性菜单。
|
||||
- `new SetMainMenuBar { menu = items, show = true }.IssueToDefault()`:主窗口菜单。
|
||||
- `new SetWorkspaceMenuBar { viewport = vp, menu = items, show = true }.Issue()`:viewport 菜单。
|
||||
- `new SetPanelMenuBar { panel = p, menu = items }.Issue()`:面板持久菜单。
|
||||
|
||||
```csharp
|
||||
pb.MenuBar(new List<MenuItem>
|
||||
{
|
||||
new("File", subItems: new()
|
||||
{
|
||||
new("Open", () => { }),
|
||||
new("-"),
|
||||
new("Exit", () => panel.Exit()),
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
完整签名见 `api-signatures.md`。
|
||||
@@ -0,0 +1,151 @@
|
||||
# 项目搭建、引用策略和启动流程
|
||||
|
||||
## 三类项目
|
||||
|
||||
1. **CycleGUI 同解决方案内示例**:例如 `LearnCycleGUI`,可 `ProjectReference` 到 `CycleGUI\CycleGUI.csproj`。
|
||||
2. **宿主/调度程序**:例如 `SimpleLite`,引用真实 `CycleGUI.dll`,用 `Costura.Fody` 把托管程序集包进宿主 Assembly。
|
||||
3. **插件或二次开发项目**:默认引用 `RefCycleGUI.dll`,不要引用真实 `CycleGUI.dll`。真实实现由宿主提供,以保护和隔离 CycleGUI 代码资产。
|
||||
|
||||
## 插件/二开项目模板
|
||||
|
||||
```xml
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="CycleGUI">
|
||||
<HintPath>$(CGUILibDir)RefCycleGUI.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
`Private=false` 用于避免把 Ref 程序集复制成插件自己的运行时依赖。宿主进程负责提供真实 CycleGUI。
|
||||
|
||||
## 宿主程序模板
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Costura.Fody" Version="5.7.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<Reference Include="CycleGUI">
|
||||
<HintPath>$(CGUILibDir)CycleGUI.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
`FodyWeavers.xml`:
|
||||
|
||||
```xml
|
||||
<Weavers>
|
||||
<Costura />
|
||||
</Weavers>
|
||||
```
|
||||
|
||||
native 依赖如 `libVRender.dll` 按宿主自己的资源、输出目录或加载策略处理,不要把插件二开项目设计成显式部署真实 `CycleGUI.dll`。
|
||||
|
||||
## 同解决方案示例模板
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\CycleGUI\CycleGUI.csproj" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
只在跟 CycleGUI 源码一起开发和验证的示例中使用该模式。
|
||||
|
||||
## 常用 using
|
||||
|
||||
```csharp
|
||||
using CycleGUI;
|
||||
using CycleGUI.API;
|
||||
using CycleGUI.Terminals;
|
||||
using System.Drawing;
|
||||
using System.Numerics;
|
||||
```
|
||||
|
||||
## 本地启动
|
||||
|
||||
```csharp
|
||||
Terminal.RegisterRemotePanel(CreateMainPanel);
|
||||
|
||||
LocalTerminal.SetTitle("MyApp");
|
||||
LocalTerminal.SetIcon(icoBytes, "MyApp");
|
||||
LocalTerminal.AddMenuItem("Exit", LocalTerminal.Terminate);
|
||||
LocalTerminal.Start();
|
||||
GUI.PromptPanel(CreateMainPanel(GUI.defaultTerminal));
|
||||
```
|
||||
|
||||
`Terminal.RegisterRemotePanel` 让 TCP/Web 终端连接后也能拿到欢迎面板。`GUI.PromptPanel` 显示本地默认终端上的主面板。
|
||||
|
||||
## Web 启动
|
||||
|
||||
```csharp
|
||||
Task.Run(() => WebTerminal.Use(port: 8081, ico: icoBytes));
|
||||
```
|
||||
|
||||
可配合 `LeastServer` 发布静态文件和简单 HTTP API。Web 细节见 `terminals-and-web.md`。
|
||||
|
||||
## 图标设置(exe / 托盘 / Web favicon)
|
||||
|
||||
三处图标**统一用 `.ico` 格式**(Windows 图标;建议内含 16/32/48 多尺寸,需要高清可再加 256)。常见做法是一个 `.ico` 同时用于三处。
|
||||
|
||||
1. **exe 可执行程序图标**:csproj 里 `<ApplicationIcon>`,编译期写入 PE 资源,资源管理器/任务栏看到的就是它。
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<ApplicationIcon>res\app_icon.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
2. **LocalTerminal 托盘 / 窗口图标**:`LocalTerminal.SetIcon(byte[] icoBytes, string name)`,传 `.ico` 文件字节,须在 `LocalTerminal.Start()` 之前调用。
|
||||
|
||||
3. **WebTerminal 浏览器 favicon**:`WebTerminal.Use(int port, byte[] ico = null)`,把同一份 `.ico` 字节作为 `ico:` 传入。
|
||||
|
||||
`icoBytes` 的两种来源:
|
||||
|
||||
```csharp
|
||||
// A) 把 .ico 作为嵌入资源(csproj: <EmbeddedResource Include="app_icon.ico" />),运行时读出
|
||||
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);
|
||||
}
|
||||
|
||||
// B) 直接从磁盘文件读
|
||||
byte[] icoBytes = File.ReadAllBytes("res/app_icon.ico");
|
||||
```
|
||||
|
||||
嵌入式(A)适合单文件分发,图标随程序集走、不依赖外部文件;磁盘式(B)适合图标可替换的场景。
|
||||
|
||||
完整三处一起设置:
|
||||
|
||||
```xml
|
||||
<!-- .csproj -->
|
||||
<PropertyGroup>
|
||||
<ApplicationIcon>res\app_icon.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="res\app_icon.ico" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
```csharp
|
||||
var icoBytes = LoadIcon();
|
||||
LocalTerminal.SetTitle("MyApp");
|
||||
LocalTerminal.SetIcon(icoBytes, "MyApp"); // 托盘 + 窗口
|
||||
LocalTerminal.Start();
|
||||
WebTerminal.Use(port: 8081, ico: icoBytes); // Web favicon
|
||||
```
|
||||
|
||||
> `SetIcon` / `WebTerminal.Use` 只接受 `.ico` 字节,不要传 PNG/JPG 字节。要从 PNG 生成,请先离线转成多尺寸 `.ico`。
|
||||
@@ -0,0 +1,86 @@
|
||||
# 终端和 Web
|
||||
|
||||
## 终端类型
|
||||
|
||||
- `LocalTerminal`:桌面原生窗口,使用 `LocalTerminal.Start()`。
|
||||
- `WebTerminal`:浏览器/WebSocket 终端,使用 `WebTerminal.Use(port, ico)`。
|
||||
- `TCPTerminal`:TCP 远程终端,使用 `TCPTerminal.Serve(port)`。
|
||||
|
||||
## 本地窗口
|
||||
|
||||
```csharp
|
||||
LocalTerminal.SetTitle("MyApp");
|
||||
LocalTerminal.SetIcon(icoBytes, "MyApp");
|
||||
LocalTerminal.AddMenuItem("Exit", LocalTerminal.Terminate);
|
||||
LocalTerminal.Start(); // 或 LocalTerminal.Start(hideAfterInit: true)
|
||||
GUI.PromptPanel(CreateMainPanel(GUI.defaultTerminal));
|
||||
```
|
||||
|
||||
`LocalTerminal.Start()` 应在 `GUI.PromptPanel` 之前调用。`SetIcon(byte[] icoBytes, string name)` 设置托盘和窗口图标。
|
||||
|
||||
### 启动后隐藏 libVRender 窗口 / 控制台
|
||||
|
||||
`LocalTerminal.Start(bool hideAfterInit = false)` 的 `hideAfterInit` 为 `true` 时:libVRender **仍会启动**,首帧渲染后自动隐藏主窗口(`glfwHideWindow`),渲染循环继续;托盘双击可恢复。这是**隐藏**,不是终止渲染后端。
|
||||
|
||||
控制台隐藏需应用自行处理(例如 Detour/Medulla 用 `ShowWindow(GetConsoleWindow(), SW_HIDE)`),与 `hideAfterInit` 无关。
|
||||
|
||||
常见做法是把开关放进工作目录 JSON,由静态构造函数或启动代码读取后传入 `LocalTerminal.Start`:
|
||||
|
||||
| 应用 | 配置文件 | 字段 |
|
||||
| --- | --- | --- |
|
||||
| DetourLite | `detourconsole.json` | `MinimizeToTray`、`HideConsoleOnStart`、`CPort` |
|
||||
| Medulla | `medullaconsole.json` | `MinimizeToTray`、`HideConsoleOnStart` |
|
||||
|
||||
示例(启动 libVRender 后立即隐藏主窗口):
|
||||
|
||||
```json
|
||||
{
|
||||
"HideConsoleOnStart": false,
|
||||
"MinimizeToTray": true
|
||||
}
|
||||
```
|
||||
|
||||
Detour 配置细节见 `detour-configuration` skill;Medulla 见 `medulla-startup-config` skill。
|
||||
|
||||
## WebTerminal
|
||||
|
||||
```csharp
|
||||
Terminal.RegisterRemotePanel(CreateMainPanel);
|
||||
Task.Run(() => WebTerminal.Use(port: 8081, ico: icoBytes));
|
||||
```
|
||||
|
||||
`RegisterRemotePanel` 让 Web/TCP 连接可以创建欢迎面板。`ico:` 是浏览器 favicon。
|
||||
|
||||
## 图标 icoBytes
|
||||
|
||||
`SetIcon` 和 `WebTerminal.Use(ico:)` 都接受 **`.ico` 文件字节**(不接受 PNG/JPG)。连同 csproj 的 `<ApplicationIcon>`(exe 图标),三处统一用一个 `.ico` 即可。`icoBytes` 可从嵌入资源或磁盘读取:
|
||||
|
||||
```csharp
|
||||
static byte[] LoadIcon() // 嵌入资源:csproj 里 <EmbeddedResource Include="app_icon.ico" />
|
||||
{
|
||||
var asm = Assembly.GetExecutingAssembly();
|
||||
using var s = asm.GetManifestResourceStream(
|
||||
asm.GetManifestResourceNames().First(p => p.Contains(".ico")));
|
||||
return new BinaryReader(s).ReadBytes((int)s.Length);
|
||||
}
|
||||
// 或:byte[] icoBytes = File.ReadAllBytes("res/app_icon.ico");
|
||||
```
|
||||
|
||||
三处图标(exe / 托盘 / web favicon)的完整设置见 `project-setup-and-packaging.md` 的「图标设置」。
|
||||
|
||||
## LeastServer
|
||||
|
||||
`WebTerminal.Use()` 内部启动 `LeastServer` 并提供 WebSocket 路由和 webVRender 页面。可增加静态文件和简单 HTTP API:
|
||||
|
||||
```csharp
|
||||
LeastServer.AddServingFiles("/static", "path/to/htdocs");
|
||||
LeastServer.AddGetHandler("/api/status", () => "OK");
|
||||
LeastServer.AddPostTextHandler("/api/data", body => ProcessData(body));
|
||||
```
|
||||
|
||||
## 多终端注意事项
|
||||
|
||||
- `IssueToDefault()` 发给默认终端。
|
||||
- `IssueToAllTerminals()` 发给所有终端。
|
||||
- `panel.Terminal` 可用于把弹窗、Painter 或操作限定到当前终端。
|
||||
- Painter 需要终端隔离时设置 `painter.terminal = specificTerminal`。
|
||||
@@ -0,0 +1,140 @@
|
||||
# Workspace 交互、对象选择和坐标拾取
|
||||
|
||||
## namePattern 语法
|
||||
|
||||
所有 `namePattern` 使用 glob 语义,不是 regex:
|
||||
|
||||
```csharp
|
||||
selectOp.SetObjectSelectable("UISite-*"); // 命中 UISite-3
|
||||
selectOp.SetObjectSelectable("UISite-.*"); // 错:这不是 regex
|
||||
```
|
||||
|
||||
需要正则时,在 C# 侧用 `Regex` 先筛出具体名字,再逐个传具体名字给 CycleGUI API。
|
||||
|
||||
## 对象选择
|
||||
|
||||
画布或 Workspace 中的业务对象需要点击、选择、框选或刷选时,优先把它们绘制成 CycleGUI 原生可选对象,然后使用 `SelectObject`。常见做法是用 `PutModelObject`、`PutPointCloud`、`PutHandleIcon`、`PutStraightLine`,或其它可被 `SetObjectSelectable` 命中的 Workspace/Painter 对象来承载可视化主体。
|
||||
|
||||
无特殊情况,不要用 `GetPosition` 读取鼠标坐标再手写命中测试来选择画布对象。`GetPosition` 只用于自由坐标拾取、放置点、测量、绘制轮廓、吸附到点位等确实需要返回坐标的位置交互。
|
||||
|
||||
Painter 更适合调试绘制和非交互 overlay;需要被用户点击选择的业务对象,应尽量有对应的原生可选对象作为主表示。
|
||||
|
||||
```csharp
|
||||
var selectOp = new SelectObject { fineSelectOnPointClouds = true };
|
||||
selectOp.feedback = (result, op) =>
|
||||
{
|
||||
foreach (var (name, selector, firstSub) in result)
|
||||
Console.WriteLine(name);
|
||||
};
|
||||
selectOp.Start();
|
||||
selectOp.SetSelectionMode(SelectObject.SelectionMode.Click);
|
||||
selectOp.SetObjectSelectable("UISite-*");
|
||||
```
|
||||
|
||||
`SetSelectionMode` 支持 `Click`、`Rectangle`、`Paint`。
|
||||
|
||||
修饰键语义:Shift + 左键追加选择,Alt + 左键从选择集中移除,左键点击空白清空选择。不要重新发明 Ctrl 多选;Ctrl 留给其它操作。
|
||||
|
||||
## sticky pattern
|
||||
|
||||
`SetObjectSelectable(pattern)` 是 sticky:
|
||||
|
||||
- 调用时对已存在对象做一次 glob 匹配并设为可选。
|
||||
- 之后新增对象时自动按该 pattern 匹配并设为可选。
|
||||
- 不需要在每次 `Workspace.AddProp` 后重复调用。
|
||||
|
||||
取消 sticky 必须用同一个 pattern 字符串:
|
||||
|
||||
```csharp
|
||||
selectOp.SetObjectUnselectable("UISite-*");
|
||||
```
|
||||
|
||||
`SetObjectUnselectable("UISite-3")` 只清具体对象,不会移除 `"UISite-*"` 的 sticky 规则。
|
||||
|
||||
## 拾取世界坐标
|
||||
|
||||
```csharp
|
||||
var gp = new GetPosition
|
||||
{
|
||||
method = GetPosition.PickMode.GridPlane,
|
||||
snaps = new[] { "UISite-*" },
|
||||
realtime = true,
|
||||
};
|
||||
|
||||
gp.feedback = (wp, _) =>
|
||||
{
|
||||
var xy = new Vector2(wp.mouse_pos.X, wp.mouse_pos.Y);
|
||||
};
|
||||
gp.finished = () => { };
|
||||
gp.terminated = () => { };
|
||||
gp.Start();
|
||||
```
|
||||
|
||||
`realtime = true` 时,鼠标移动也会触发 `feedback`,适合用 Painter 做橡皮筋预览;最终左键点击时再触发 `finished`。
|
||||
|
||||
## SelectObject 和 GetPosition 互斥
|
||||
|
||||
Workspace 输入由当前 UI operation 消费。进入放置/拾取模式前先 `selectOp.End()`;结束后再创建并启动新的 `SelectObject`。sticky pattern 是 per-op 状态,重启选择 op 后需要重新设置 pattern。
|
||||
|
||||
## async/await 包装
|
||||
|
||||
把 `WorkspaceUIOperation` 包成 `Task` 时,`feedback` 只更新数据或 `TrySetResult`,不要在 `feedback` 后半段重启新 op 或做全局清理。清理放到调用方 `try/finally`。
|
||||
|
||||
推荐模式:
|
||||
|
||||
```csharp
|
||||
private static Task<Vector2?> PickPoint(State st, Action<Vector2>? onPreview = null)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<Vector2?>();
|
||||
Vector2 last = default;
|
||||
bool got = false;
|
||||
|
||||
var gp = new GetPosition { realtime = onPreview != null };
|
||||
gp.feedback = (wp, _) =>
|
||||
{
|
||||
last = new Vector2(wp.mouse_pos.X, wp.mouse_pos.Y);
|
||||
got = true;
|
||||
onPreview?.Invoke(last);
|
||||
};
|
||||
gp.finished = () => tcs.TrySetResult(got ? last : null);
|
||||
gp.terminated = () => tcs.TrySetResult(null);
|
||||
gp.Start();
|
||||
st.CurrentOp = gp;
|
||||
return tcs.Task;
|
||||
}
|
||||
```
|
||||
|
||||
业务取消按钮必须显式唤醒 pending task:
|
||||
|
||||
```csharp
|
||||
st.CurrentOp?.End();
|
||||
st.PendingTcs?.TrySetResult(null);
|
||||
```
|
||||
|
||||
`End()` 不会触发 `terminated`。
|
||||
|
||||
## Guizmo 自由度限制
|
||||
|
||||
`GuizmoAction` 使用 `dof` 声明允许的平移/旋转自由度。CycleGUI 是 Z-up 语义,`Yaw` 等价于 `RotateZ`,常用于地面 XY 平面编辑。
|
||||
|
||||
```csharp
|
||||
new GuizmoAction
|
||||
{
|
||||
dof = GuizmoAction.GuizmoDof.PlanarXYYaw,
|
||||
realtimeResult = true,
|
||||
feedback = (items, _) =>
|
||||
{
|
||||
foreach (var (name, pos, rot) in items)
|
||||
Console.WriteLine($"{name}: {pos}, {rot}");
|
||||
},
|
||||
}.Start();
|
||||
```
|
||||
|
||||
常用组合:
|
||||
|
||||
- `TranslateXYZ`:只允许三轴平移。
|
||||
- `PlanarXY`:只允许 X/Y 平面平移。
|
||||
- `PlanarXYYaw`:只允许 X/Y 平移和绕 Z 轴旋转。
|
||||
- `Yaw`:只允许绕 Z 轴旋转。
|
||||
- `RotateXYZ`:只允许三轴旋转。
|
||||
- `All`:允许三轴平移和三轴旋转。
|
||||
@@ -0,0 +1,129 @@
|
||||
# Workspace 场景、Viewport 和 Painter
|
||||
|
||||
## 颜色契约
|
||||
|
||||
CycleGUI 公开颜色字段统一使用 `System.Drawing.Color`。不要把 ARGB `uint` 直接塞给颜色字段;旧 wire-format 用 `Extensions.FromRgba8(uint)` 转回 `Color`。
|
||||
|
||||
## 模型加载和实例
|
||||
|
||||
```csharp
|
||||
Workspace.AddProp(new LoadModel
|
||||
{
|
||||
name = "robot_class",
|
||||
detail = new Workspace.ModelDetail(File.ReadAllBytes("robot.glb"))
|
||||
{
|
||||
Scale = 0.01f,
|
||||
Center = Vector3.Zero,
|
||||
}
|
||||
});
|
||||
|
||||
Workspace.AddProp(new PutModelObject
|
||||
{
|
||||
name = "robot_1",
|
||||
clsName = "robot_class",
|
||||
newPosition = new Vector3(1, 0, 0),
|
||||
newQuaternion = Quaternion.Identity,
|
||||
});
|
||||
```
|
||||
|
||||
用 `TransformObject` 更新位置和姿态:
|
||||
|
||||
```csharp
|
||||
Workspace.AddProp(new TransformObject
|
||||
{
|
||||
name = "robot_1",
|
||||
pos = new Vector3(2, 0, 0),
|
||||
quat = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathF.PI / 4),
|
||||
timeMs = 500,
|
||||
});
|
||||
```
|
||||
|
||||
## 点云
|
||||
|
||||
```csharp
|
||||
Workspace.AddProp(new PutPointCloud
|
||||
{
|
||||
name = "scan",
|
||||
xyzSzs = points.Select(p => new Vector4(p, 3f)).ToArray(),
|
||||
colors = points.Select(_ => Color.White).ToArray(),
|
||||
newPosition = Vector3.Zero,
|
||||
});
|
||||
```
|
||||
|
||||
`xyzSzs` 的 `Vector4.W` 是点大小。颜色数组使用 `Color[]`。
|
||||
|
||||
## 线、文本和对象删除
|
||||
|
||||
常用 prop 可在 `api-signatures.md` 查询。删除对象可保留 prop 引用调用 `Remove()`,或使用 name pattern 删除。
|
||||
|
||||
```csharp
|
||||
myProp.Remove();
|
||||
WorkspaceProp.RemoveNamePattern("robot_*");
|
||||
```
|
||||
|
||||
name pattern 是 glob,不是 regex。
|
||||
|
||||
## 相机和外观
|
||||
|
||||
```csharp
|
||||
new SetCamera
|
||||
{
|
||||
lookAt = Vector3.Zero,
|
||||
azimuth = -MathF.PI / 2,
|
||||
altitude = MathF.PI / 4,
|
||||
distance = 10f,
|
||||
fov = 45f,
|
||||
projectionMode = SetCamera.ProjectionMode.Perspective,
|
||||
}.IssueToAllTerminals();
|
||||
```
|
||||
|
||||
```csharp
|
||||
new SetAppearance
|
||||
{
|
||||
useEDL = true,
|
||||
useSSAO = true,
|
||||
useGround = true,
|
||||
drawGroundGrid = true,
|
||||
drawGuizmo = true,
|
||||
useDefaultSky = true,
|
||||
}.IssueToDefault();
|
||||
```
|
||||
|
||||
## Viewport
|
||||
|
||||
```csharp
|
||||
var vp = GUI.PromptWorkspaceViewport(
|
||||
panel => panel.ShowTitle("Aux View"),
|
||||
closeEvent: () => true);
|
||||
|
||||
vp.OnClosing(() => true);
|
||||
```
|
||||
|
||||
不传 `closeEvent` 时不显示关闭按钮。
|
||||
|
||||
## Painter
|
||||
|
||||
Painter 用于调试绘制,数据按帧刷新,不持久化为业务对象。
|
||||
|
||||
```csharp
|
||||
var p = Painter.GetPainter("debug");
|
||||
p.Clear();
|
||||
p.DrawDot(Color.Red, new Vector3(1, 0, 0), size: 3f);
|
||||
p.DrawLine(Color.Yellow, start, end, width: 2f, arrow: Painter.ArrowType.End);
|
||||
p.DrawVector(origin, direction, Color.Green, width: 1f, pixels: 20);
|
||||
p.DrawText(Color.White, new Vector3(0, 0, 1), "Label");
|
||||
p.DrawRegion3D(Color.Blue, center);
|
||||
```
|
||||
|
||||
同名 Painter 复用同一层。需要限定终端时设置 `painter.terminal = terminal`。
|
||||
|
||||
## 填充多边形
|
||||
|
||||
```csharp
|
||||
p.DrawPolygonFilled(points2D, translation, rotation,
|
||||
borderColor: Color.White,
|
||||
fillColor: Color.FromArgb(128, Color.Blue),
|
||||
thickness: 0.1f);
|
||||
```
|
||||
|
||||
更多 prop 和 Painter 签名见 `api-signatures.md`。
|
||||
+3
-1
@@ -364,4 +364,6 @@ FodyWeavers.xsd
|
||||
build/
|
||||
/build/SimpleComposer.exe
|
||||
/build/plugins/StandardScene.dll
|
||||
/.cursor
|
||||
/.cursor/*
|
||||
!/.cursor/skills/
|
||||
!/.cursor/skills/**
|
||||
|
||||
Reference in New Issue
Block a user