sapdon 3.4.0 → 3.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/doc/dev/core.md CHANGED
@@ -132,7 +132,7 @@ src/core/
132
132
  │ ├── dataBindingObject.js # DataBindingObject 类
133
133
  │ ├── elements/ # UI 元素 (UIElement, Panel, Button, Image, Label, Grid...)
134
134
  │ ├── properties/ # UI 属性 (Control, Layout, DataBinding, Sprite, Text...)
135
- │ └── systems/ # UI 系统 (UISystem, Chest, ServerForm, Guidebook, HUD, NeoGuidebook...)
135
+ │ └── systems/ # UI 系统 (UISystem, Chest, ServerForm, Guidebook, HUD, SapdonGuideBook...)
136
136
 
137
137
  └── extra/ # ── 附加模块 ──
138
138
  ├── apperance.ts # ClientEntityApperance (实体外观管理)
@@ -683,11 +683,10 @@ UIElement (基类: name, type, template, control, layout, properties)
683
683
  | 系统 | 文件 | 说明 |
684
684
  |------|------|------|
685
685
  | `UISystem` | `systems/system.js` | 核心 UI 文件系统,管理 elements + animations |
686
- | `ServerFormSystem` | `systems/serverForm.js` | 预置服务器表单 UI |
686
+ | `SapdonServerUI` | `systems/sapdon/sapdonServerUI.ts` | 页面壳路由系统,生成 `server_form.json` |
687
687
  | `ChestUISystem` | `systems/chest.js` | 容器 UI 系统 |
688
688
  | `ContainerUISystem` | `systems/containerUISystem.js` | 自定义容器 UI |
689
- | `Guidebook` | `systems/guidebook.js` | 指南书 UI |
690
- | `NeoGuidebook` | `systems/neoGuibook/book.ts` | 新版指南书 UI,详见 [neo-guidebook.md](neo-guidebook.md) |
689
+ | `SapdonGuideBook` | `systems/sapdon/sapdonGuideBook.ts` | 数据驱动手册框架类,详见 [guidebook.md](../guidebook.md) |
691
690
  | `HudUISystem` | `systems/hud/hud.ts` | HUD 系统 |
692
691
  | `HudStatePanel` | `systems/hud/hudElement.ts` | HUD 状态面板 |
693
692
 
@@ -0,0 +1,274 @@
1
+ # UI 子系统架构(core/ui)
2
+
3
+ Sapdon 的 UI 子系统把 Minecraft Bedrock 原生的 **JSON UI 声明式 schema**,翻译成一套**类型安全、链式调用、可序列化**的 TypeScript 对象模型;构建期把一个界面序列化成 `RP/ui/*.json` 包体,并注册进 `ui/_ui_defs.json`。原生是手写 JSON,Sapdon 用 TS 对象组出来再吐回 JSON。
4
+
5
+ > 本文是框架开发者视角的设计梳理。使用层面的 API 参考 [`doc/user/api/sapdon-ui.md`](../user/api/sapdon-ui.md) 与 [`doc/user/tutorials/sapdon-ui.md`](../user/tutorials/sapdon-ui.md)。
6
+
7
+ ---
8
+
9
+ ## 1. 文件布局
10
+
11
+ ```
12
+ src/core/ui/
13
+ ├── index.ts # 聚合导出
14
+ ├── types.ts # 共享类型:Size2 / Offset2 / Anchor / Binding* / ModificationOperation
15
+ ├── buttonMapping.ts # ButtonMapping 输入映射类
16
+ ├── dataBindingObject.ts # DataBindingObject 数据绑定配置类
17
+ ├── elements/ # 元素层:UIElement + Panel/StackPanel/Grid/Label/Image/Button/...
18
+ ├── properties/ # 属性包层:Control/Layout/Text/Sprite/Input/Sound/DataBinding/GridProp/ScrollView/Factory
19
+ ├── registry/
20
+ │ └── uiSystemRegistry.ts # UISystemRegistry:注册 .json 文件并维护 _ui_defs 列表
21
+ ├── extra/
22
+ │ └── hudProgressBar.ts # HudProgressBar(组合 HudStatePanel + Sprite 裁切)
23
+ └── systems/ # 系统层:UISystem + 各类落地系统
24
+ ├── system.ts # UISystem = 一个 UI 文件(.json),序列化入口
25
+ ├── chest.ts # ChestUISystem(接管 vanilla 箱子 screen)
26
+ ├── containerUISystem.ts # ContainerUISystem(自定义容器 UI)
27
+ ├── hud/ # HudUISystem + HudStatePanel
28
+ ├── neoGuibook/ # (旧)NeoGuidebook + NeoGuidebookPage(兼容保留,新用 SapdonGuideBook)
29
+ └── sapdon/ # SapdonServerUI(表单路由壳)+ SapdonPanel/FormButton/FormButtonGrid
30
+ ```
31
+
32
+ 另:工厂入口 `src/core/factory/uiFactory.js`(`UiAPI`)提供 `createUISystem / createPanel / createImage / createLabel` 等创建入口。
33
+
34
+ ---
35
+
36
+ ## 2. 四层对象模型
37
+
38
+ ```
39
+ ┌──────────────────────────────────────────────────────────┐
40
+ │ Layer 4: systems/ UISystem + 落地系统 │
41
+ │ UISystem = 一个 UI 文件;ContainerUISystem / SapdonServerUI │
42
+ │ / SapdonGuideBook / HudUISystem 等在其上叠业务能力 │
43
+ ├──────────────────────────────────────────────────────────┤
44
+ │ Layer 3: elements/ 元素层 │
45
+ │ UIElement 基类 + Panel/StackPanel/Grid/Label/Image/Button │
46
+ │ serialize() → { id: json };组合若干属性包 │
47
+ ├──────────────────────────────────────────────────────────┤
48
+ │ Layer 2: properties/ 属性包层 │
49
+ │ Control/Layout/Text/Sprite/Input/Sound/DataBinding/... │
50
+ │ 每组 setXxx() 链式方法,对应 JSON UI 的一个字段命名空间 │
51
+ ├──────────────────────────────────────────────────────────┤
52
+ │ Layer 1: types.ts 类型层 │
53
+ │ Size2/Offset2/Anchor/Binding*/Modification 等字面量类型 │
54
+ └──────────────────────────────────────────────────────────┘
55
+ ```
56
+
57
+ **序列化链路**(构建期,`main.ts` 里的 `registry.submit()` 驱动):
58
+
59
+ ```
60
+ main.ts 组元素
61
+ → UIElement.serialize()
62
+ 合并 serializableSources()(属性包逐份拷贝进 properties Map)
63
+ → { [id]: json }(variables 受 $ 前缀并入)
64
+ → UISystem.addElement() 存进 elements Map
65
+ → UISystem.toObject()(@Serializer)按 id 铺开
66
+ → UISystemRegistry.registerUISystem()
67
+ 注册 .json 文件 + 刷新 _ui_defs.json 的 ui_defs 列表
68
+ → 构建管道落盘 dev/<proj>_RP/ui/<name>.json + _ui_defs.json
69
+ ```
70
+
71
+ ### 2.1 类型层 `types.ts`
72
+
73
+ JSON UI 规范的受控映射,避免魔法字符串:
74
+
75
+ | 类型 | 说明 |
76
+ |------|------|
77
+ | `Size2` / `Offset2` | 尺寸 / 偏移向量(像素数字或 `"50%"` 计算串) |
78
+ | `Anchor` | 9 个锚点字面量 |
79
+ | `BindingType` / `BindingCondition` | 数据绑定的 type / condition 枚举 |
80
+ | `ModificationOperation` | Modifications 的操作字面量 |
81
+ | `JsonUIBag` | 任意键属性包(基类索引签名基类型) |
82
+
83
+ ### 2.2 属性包层 `properties/`
84
+
85
+ 每种属性包 = JSON UI 一个字段命名空间的一组 `setXxx()` 链式方法:
86
+
87
+ | 类 | 对应字段 | 典型方法 |
88
+ |----|---------|---------|
89
+ | `Control` | 控件视觉行为 | `setVisible / setLayer / setAlpha / setClipsChildren / addControl` |
90
+ | `Layout` | 布局尺寸 | `setSize / setOffset / setAnchorFrom/To / setDraggable / setContained` |
91
+ | `Text` | label 文本 | `setText / setColor / setShadow / setTextAlignment` |
92
+ | `Sprite` | image 纹理 | `setTexture / setUV / setUVSize / setClipDirection / setTiled` |
93
+ | `Input` | 输入 | `setButtonMappings / setModal / setAlwaysListenToInput` |
94
+ | `Sound` | 音效 | 按钮点击音效等 |
95
+ | `DataBinding` | 数据绑定容器 | `addDataBinding` |
96
+ | `GridProp` | 网格布局 | `setGridDimensions / setGridItemTemplate / setGridFillDirection` |
97
+ | `ScrollView` | 滚动条 | 滚动属性 |
98
+ | `Factory` | 模板工厂 | `setName / setControlName / setControlIds` |
99
+
100
+ ### 2.3 元素层 `elements/`
101
+
102
+ `UIElement` 基类持有属性包引用:`control`、`layout`、`dataBinding`、`factory`,外加按需的 `properties`/`variables`/`modifications` Map。核心方法:
103
+
104
+ - `serialize()`:把 `serializableSources()` 返回的属性包逐份合并进 `properties`,再并入 `$variables`,输出 `{ [id]: json }`。
105
+ - `addProp` / `addVariable` / `addControl(s)` / `addModification` / `enableDebug`(调试描边框)。
106
+
107
+ 子类通过覆写 `serializableSources()` 把专属属性包追加到序列化源里(见"组合优于继承"):
108
+
109
+ ```
110
+ 元素体系(type 字段在构造器里预置)
111
+ UIElement (基类)
112
+ ├── Panel type=panel
113
+ │ └── StackPanel type=stack_panel(加 orientation)
114
+ ├── CollectionPanel type=collection_panel
115
+ │ └── Grid type=grid(加 GridProp + addGridItem)
116
+ ├── Label type=label(+ Text)
117
+ ├── Image type=image(+ Sprite)
118
+ ├── Button type=button(+ Input/Sound/Factory)
119
+ └── ScrollingPanel type=scroll_view(+ Input/ScrollView)
120
+ ```
121
+
122
+ ### 2.4 系统层 `systems/`
123
+
124
+ `UISystem` = 一个 UI 文件:
125
+
126
+ - 构造 `(identifier, path)`:Split 出 `namespace`/`name`,随即 `UISystemRegistry.registerUISystem(this)`。
127
+ - `addElement / getElement / addAnimation / toObject()`:`toObject()` 打上 `@Serializer`,遍历 elements 展开成 `{ [id]: json }`,并以 `namespace` 开头。
128
+
129
+ `UISystemRegistry` 维护全局 map + `_ui_defs` 列表:
130
+
131
+ ```typescript
132
+ registerUISystem(ui_system) {
133
+ const path = ui_system.path + ui_system.name + '.json'
134
+ map[path] = ui_system; def_list.push(path)
135
+ GRegistry.register(ui_system.name, 'resource', ui_system.path, ui_system)
136
+ GRegistry.register('_ui_defs', 'resource', 'ui/', { ui_defs: def_list })
137
+ }
138
+ addOuterUIdefs(ui_defs) // 追加外部原版 ui_def
139
+ ```
140
+
141
+ > 任一 UI 文件(`ChestUISystem.chest_screen`、`HudUISystem`、`SapdonServerUI`、用户页面等)在模块加载/构造时注册进 `GRegistry`,故 `main.ts` 末行 `registry.submit()` 会统一把它们落到 `RP/ui/`。
142
+
143
+ ---
144
+
145
+ ## 3. 五个核心设计机制
146
+
147
+ ### 3.1 组合优于继承
148
+
149
+ `UIElement` 不把全部字段堆在类里,而是**组合**若干属性包(`Control/Layout/DataBinding` 等)。子类只在自己那一层追加专属包:
150
+
151
+ ```typescript
152
+ // Button.serializableSources()
153
+ [this.layout, this.input, this.sound, this.dataBinding, this.factory, this.control]
154
+ // Grid(在 CollectionPanel 之上)再前置 GridProp
155
+ [this.grid, ...super.serializableSources()]
156
+ ```
157
+
158
+ 好处:字段归属清晰、复用度高、子类只声明"我多了什么"。
159
+
160
+ ### 3.2 模板继承 `id@template`
161
+
162
+ 构造函数签名 `(id, template?)`。传 template 时 `id = name@template`,直接继承原版/内置模板,只做增量覆盖:
163
+
164
+ ```typescript
165
+ const formButton = new Button('form_button', 'common_buttons.light_text_button')
166
+ new UIElement('content', undefined, '$user_content_panel') // 模板/变量作模板
167
+ new UIElement('common_panel@common.common_panel', undefined)
168
+ ```
169
+
170
+ ### 3.3 Modifications 非侵入修改
171
+
172
+ `Modifications.OPERATION.*`(`insert_back/insert_front/insert_after/.../replace/remove`)+ `UIElement.addModification({array_name, operation, value})`。用于非侵入式改写 vanilla 或其他包的 JSON UI,保持兼容:
173
+
174
+ ```typescript
175
+ // 往 vanilla 的 main_screen_content.controls 末尾追加自定义 factory
176
+ new UIElement('main_screen_content').addModification({
177
+ array_name: 'controls', operation: Modifications.OPERATION.INSERT_BACK,
178
+ value: [sapdonFormFactory.serialize()],
179
+ })
180
+ ```
181
+
182
+ ### 3.4 数据绑定 DSL
183
+
184
+ `DataBindingObject` 把 JSON UI 的 `bindings`(`global / view / collection / collection_details`)映射为 TS,配合 `addVariable` 的 `$xxx` 做运行时可见性门控。核心模式是 `view` 绑定 + 比较表达式写进 `source_property_name`:
185
+
186
+ ```typescript
187
+ elem.addVariable('binding_text', pageId)
188
+ elem.dataBinding.addDataBinding(
189
+ new DataBindingObject().setBindingType('view')
190
+ .setSourcePropertyName('($binding_text = #form_text)')
191
+ .setTargetPropertyName('#visible')
192
+ )
193
+ ```
194
+
195
+ 运行时由 Script API 的 `ActionFormData().body() / .button()` 把值 emit 进 `#form_text` / `#form_button_text`,UI 侧据此显隐 → **"UI 逻辑零 JS,全在绑定表达式里"**。
196
+
197
+ ### 3.5 工厂统一入口
198
+
199
+ `src/core/factory/uiFactory.js` 暴露 `UiAPI`(`createUISystem / createPanel / createImage / createLabel` 等),与 ItemAPI/EntityAPI 等并列。多数场景直接 `new UISystem(...)` / `new Panel(...)` 更灵活,工厂是兜底入口。
200
+
201
+ ---
202
+
203
+ ## 4. 两条高层落地路线
204
+
205
+ | 形式 | 走法 | 代表 |
206
+ |------|------|------|
207
+ | **server_form(表单 / 容器 / 书)** | `SapdonServerUI` 用 modification 把自定义页注入 vanilla `main_screen_content`,用 `#title_text` / `#form_text` 前缀门控可见性;每页 = "内容面板 + 按键面板" | `SapdonGuideBook`、`ContainerUISystem/ChestUISystem`、`SapdonPanel/FormButton/FormButtonGrid` |
208
+ | **HUD 常驻** | `HudUISystem` 改 vanilla `hud_title_text` 绑定,`mountRootElement` 往根面板 `insert_front` 挂元素;`HudStatePanel` 用 title 字符串做状态机驱动 `#visible` | `HudProgressBar` |
209
+
210
+ ### 4.1 server_form 路由壳(`SapdonServerUI`)
211
+
212
+ 采用 Bedrock Wiki Action Form 官方路由:
213
+
214
+ ```
215
+ main_screen_content(size:[fill,fill]) ─(modification: controls.insert_back)→ sapdon_form_factory
216
+ └─ factory{ server_form_factory, long_form } → @server_form.sapdon_long_form_panel
217
+ └─ (modifications controls.insert_back) 所有注册页 Panel($panel_id 前缀门控,扁平化)
218
+ ├─ content@$user_content_panel (下)
219
+ └─ buttons@$user_buttons_panel (上)
220
+ long_form ─(modification: bindings)→ title 含 'sapdon_ui:' 时隐藏原生表单
221
+ ```
222
+
223
+ 关键收益:自定义页处于 `main_screen_content` 作用域,`#form_text` / `#title_text` 均可解析。
224
+
225
+ ### 4.2 HUD 常驻(`HudUISystem` + `HudStatePanel`)
226
+
227
+ `HudStatePanel` 的核心是"状态字符串"驱动:根面板定义 `$update_string`,监听 `#hud_title_text_string` 变化;每个 `addStateControl(state, control)` 给子控件挂两条 view 绑定——一条回填文本、一条 `(#text = 'ui.hud.<name>.<state>') → #visible`。组件作者只需在 tick 里往 `#hud_title_text_string` 写 `<name>.<state>`,对应状态层即显隐。
228
+
229
+ ---
230
+
231
+ ## 5. 对称门控模型(`examples/guidebook_demo`)
232
+
233
+ `guidebook_demo` 演示最通用的"页面切换"落地:**内容面板与按钮面板"对称",都按当前页门控,组内按钮再按运行时 emit 的按钮文字门控**。双向、两级,UI 数据流完全由 view 绑定表达式驱动,无 JS 逻辑。
234
+
235
+ ### 5.1 模型
236
+
237
+ ```
238
+ 页面级门控(#form_text) .body(page) → 该页「内容元素 + 按钮组」一起显隐
239
+ 按钮级门控(#form_button_text) .button(text) → 组内 setBinding(text) 的按钮可见
240
+ ```
241
+
242
+ `SymGatedBook`(`src/gated_book.ts`):
243
+
244
+ - 构造时 `SapdonServerUI.registerPage({ panelId: 'sapdon_ui:book', ... })` → 页面注册进 server_form 路由壳。
245
+ - `addPage(id, content, buttons)`:一页 = 一个内容元素 + 一组 `FormButton`(`{btn, pos?}`)。
246
+ - `build()`:
247
+ - 内容面板:多页内容叠在 `gated_book_pages_panel`,每页 `gate(content, id)`。
248
+ - 按钮面板:每页一个独立 `FormButtonGrid`(grid,`collection=form_buttons`)——**按钮必须 `addButton` 进格盘绑定才有效,游离按钮无效**——逐按钮 `addButton(index, btn, pos?)` 注入集合/门控绑定,再对整个 grid `gate(built, pageId)`。
249
+
250
+ ```typescript
251
+ private gate(elem, pageId) { // 内容/按钮组共用门控
252
+ elem.addVariable('binding_text', pageId)
253
+ elem.dataBinding.addDataBinding(
254
+ new DataBindingObject().setBindingType('view')
255
+ .setSourcePropertyName('($binding_text = #form_text)')
256
+ .setTargetPropertyName('#visible'))
257
+ }
258
+ ```
259
+
260
+ `main.ts` 组装三页(page1 只有 next,page2 prev+next,page3 prev+home)后 `book.build()` + `registry.submit()`。`scripts/index.ts` 用 `ActionFormData().title('sapdon_ui:book').body(page).button(text...)` 把数据 emit 进 `#title_text/#form_text/#form_button_text`,再按 `PAGE_BUTTONS` 表做翻页跳转。
261
+
262
+ ### 5.2 产物对照
263
+
264
+ - **`dev/<proj>_RP/ui/server_form.json`**:`main_screen_content` 注入 `sapdon_form_factory`;`long_form` 按 `sapdon_ui:` 前缀隐藏原生;`sapdon_long_form_panel` 挂注册页 `book`,`$panel_id=sapdon_ui:book`,`$user_content_panel=gateddemo.book_content_panel` / `$user_buttons_panel=gateddemo.book_buttons_panel`。
265
+ - **`book.json`(ns=gateddemo)**:`book_content_panel` + `book_buttons_panel`。每页 grid 内按钮带三组 bindings:
266
+ - `collection_details`(form_buttons)+ `collection`(`#form_button_text`)→ 接 collection
267
+ - `view`:`($binding_button_text = #form_button_text) → #visible` → 按钮门控
268
+ - **`_ui_defs.json`**:汇集 `hud_screen / chest_screen / book / server_form` 四个文件。
269
+
270
+ ### 5.3 已知待清理点(demo 现状)
271
+
272
+ - **调试残留**:`FormButtonGrid(...).enableDebug()` 与 `buttons_panel.enableDebug()` 开着,产物里每个按钮/网格都带红 `debug_board` 覆盖层。发布前应关。
273
+ - **page3 按钮数不一致**:`main.ts` 给 page3 配了 `[prev, home, next]` 三个按钮,但 `scripts/index.ts` 的 `PAGE_BUTTONS.page3` 只 emit `["prev_button", "home_button"]`,故 page3 的 next 永远隐藏(符合门控语义,非 bug,但多余/易误导)。
274
+ - **`gated_form_text`**(居中 Label 直接显示 `#form_text` 原始 id)疑似调试性质。
@@ -0,0 +1,147 @@
1
+ # Sapdon UI 实战经验 / 排障与模式
2
+
3
+ 本文沉淀在 `guidebook_demo` 开发中踩过的坑与提炼出的可用模式。面向框架/UI 开发者:讲运行时机制、打破直觉的坑、以及经过验证的「`FormButton` + `FormButtonGrid`」纹理按钮方案。
4
+
5
+ > 架构总览见 `doc/dev/ui-architecture.md`;用户教程见 `doc/user/tutorials/sapdon-ui.md`;旧文本按钮 API 见 `doc/user/api/sapdon-ui.md`。
6
+
7
+ ---
8
+
9
+ ## 1. 硬约束:Server Form 的"通道预算"
10
+
11
+ Bedrock 的 Server Form 给 JSON UI 注入的东西只有这么几个:
12
+
13
+ | 通道 | 来源 | 对应绑定 |
14
+ |------|------|---------|
15
+ | 标题 | `.title(...)` | `#title_text` |
16
+ | 正文 | `.body(...)` | `#form_text` |
17
+ | 按钮 | `.button(...)` × N | `#form_button_text`(collection `form_buttons`) |
18
+
19
+ 推论:
20
+ - **JSON UI 无法解析字符串**。它只能做算术、`=` 相等、字符串 `-` 减号(可用来测前缀/包含)。
21
+ - 想"按布局类型切换"→ 用 `#form_text` 的**前缀匹配**(`((#form_text - $tag) != #form_text)` → 可见),不要试图塞 JSON。
22
+ - 想显示**第二段动态文本** → Server Form 给不了第二通道(只有 title/body),只能把内容塞进 body 或利用按钮集合。
23
+
24
+ ---
25
+
26
+ ## 2. 三个致命坑(按血泪排序)
27
+
28
+ ### 2.1 `light_text_button` 的"空 binding_name"陷阱 —— 按钮整个不渲染
29
+
30
+ 症状(运行时 ContentLog 一屏屏刷):
31
+
32
+ ```
33
+ UI Control: .../next/default/button_content/common_buttons.new_ui_binding_button_label
34
+ JSON UI parse failure: Must define a binding name!
35
+ Data bindings must have at least one property to bind!
36
+ ```
37
+
38
+ 机制:`common_buttons.light_text_button` 的文字由 `new_ui_binding_button_label` 渲染,它其中一个 binding 是:
39
+
40
+ ```json
41
+ { "binding_type": "collection", "binding_name": "$button_text", ... }
42
+ ```
43
+
44
+ `binding_name` 直接取值 **`$button_text`**。若你想"隐藏按钮文字"而设 `$button_text = ""`,`binding_name` 就变成**空串** → 解析失败 → **该按钮随整条控制链不渲染**。
45
+
46
+ > 这解释了"按钮一直不显示 / 位置不对"——它压根没渲染出来,调位置是徒劳。
47
+
48
+ 结论:**不要用 `light_text_button` 做纯图标按钮**;图标按钮用「无文字的贴图按钮」(见第 4 节 `@common.button` 方案)。
49
+
50
+ ### 2.2 `pressed_button_name` 必须是变量,不是属性
51
+
52
+ 在 `@common.button`/`common.button` 基底里,点击映射名要写成**变量**:
53
+
54
+ ```json
55
+ "$pressed_button_name": "button.form_button_click" // ✅
56
+ ```
57
+
58
+ 写成直接属性会报:
59
+
60
+ ```
61
+ Unknown property [pressed_button_name]
62
+ ```
63
+
64
+ > `.addVariable('pressed_button_name', ...)`,不是 `.addProp(...)`。
65
+
66
+ ### 2.3 `buildMode: "debug"` 不重生成 UI 产物
67
+
68
+ `build.config` 里 `buildOptions.buildMode` 填 `debug` 时,**UI JSON 不会重新生成**——`dev/.../ui/*.json` 停留在旧/残缺状态,部署后改代码看不到效果,还会把"旧 bug"误当成"我的改动导致"。
69
+
70
+ 调试 UI 请用:
71
+
72
+ ```json
73
+ "buildOptions": { "buildMode": "dev", ... }
74
+ ```
75
+
76
+ ---
77
+
78
+ ## 3. 按钮"激活三件套"绑定
79
+
80
+ 一个可通过表单点击并受门控的按钮,必须带这三组绑定(都挂在 `form_buttons` 集合上):
81
+
82
+ ```json
83
+ [
84
+ { "binding_type": "collection_details", "binding_collection_name": "form_buttons" },
85
+ { "binding_type": "collection", "binding_collection_name": "form_buttons", "binding_name": "#form_button_text" },
86
+ { "binding_type": "view", "source_property_name": "($binding_button_text = #form_button_text)", "target_property_name": "#visible" }
87
+ ]
88
+ ```
89
+
90
+ - `collection_details` + `collection` → 让按钮参与集合、可被点击并产生 `selection`;
91
+ - `view` → 仅当运行时 `.button(x)` 发射的 `#form_button_text` 等于该按钮的 `$binding_button_text` 时可见(门控)。
92
+
93
+ > 这些绑定必须在 **grid / 集合上下文**里有效;游离(不放进网格)的按钮绑定无效。这也是"按钮要加进 `FormButtonGrid` 才有用"的设计原因。
94
+
95
+ ---
96
+
97
+ ## 4. 推荐模式:`FormButton` + `FormButtonGrid`(`guidebook_demo`)
98
+
99
+ 经上述坑之后,采用"纯样式按钮 + 负责几何/绑定的格盘"。
100
+
101
+ ### 4.1 `FormButton extends Button`(纯样式)
102
+
103
+ - 基底固定 `common.button`(**无文字 label**,避开 2.1 的坑);
104
+ - 三态纹理 = 三个子控件 `default` / `hover` / `pressed`(`Image` + 实际纹理路径);
105
+ - `setTexture(d, h, p)` / `setBinding(key)` / `setAnchor(a)` / `setSize(w, h)`;
106
+ - 定位与集合绑定**不**在按钮内——交给 `FormButtonGrid`。
107
+
108
+ 要点:`FormButton.setSize` / `setAnchor` 应**原地改 layout**(`this.layout.setSize(...)`),而不是 `new Layout()` 整体替换,否则后调用会互刷。
109
+
110
+ ### 4.2 `FormButtonGrid`(几何 + 激活)
111
+
112
+ ```ts
113
+ grid = new FormButtonGrid(id, { dimensions: [c, r], size: [w, h] })
114
+ grid.addButton(index, btn, pos?) // pos 叠加式:col = index%c + pos[0], row = index/c + pos[1]
115
+ ```
116
+
117
+ - 内部一个 `Grid`,`collection_name: form_buttons`;
118
+ - `grid_position` 用基准格 `[index%c, index/c]` 锚定;
119
+ - 用**负百分 offset** `[-col*100%, -row*100%]` 把按钮修正回基坐标系(抵消 Grid 默认排布),再叠加 `pos` 移到目标格;
120
+ - `addButton` 顺手注入第 3 节的"激活三件套";`enableDebug()` 通过 `addGridItem(..., RED)` 给每个格子描红框。
121
+
122
+ `main.ts` 侧按页声明:
123
+
124
+ ```ts
125
+ book.addPage('page1', content, [
126
+ { btn: navBtn('next', 'next_button', 'bottom_right'), pos: [2, 0] },
127
+ ])
128
+ ```
129
+
130
+ ---
131
+
132
+ ## 5. 常用语义 / 属性注意事项
133
+
134
+ | 项 | 结论 |
135
+ |----|------|
136
+ | `grid_position` 顺序 | 本仓库约定 `[列, 行]`(与 `grid_dimensions [列, 行]` 一致) |
137
+ | `grid` 内子控件定位 | grid 接管子控件;要偏移包一层 pos_wrap,offset 放内层 |
138
+ | `collection_index` | **不是合法 UI 属性**(引擎报 Unknown property);集合顺序靠放置顺序 |
139
+ | 退出/非集合按钮 | 用普通 `Button("exit","common.button")` + `$pressed_button_name:"button.menu_exit"`,不入集合 |
140
+ | 调试描框 | `UIElement.enableDebug(color?)` / `Grid.addGridItem(..., [1,0,0,1])`(红) |
141
+ | 小数/百分 offset | JSON UI 接受百分数字符串;TS `setOffset([number,number])` 需 `as unknown as [number, number]` |
142
+
143
+ ---
144
+
145
+ ## 6. 一句话总结
146
+
147
+ > **按钮能不能显示,别先怪位置——先确认它到底渲染没有。** 用 `common.button` 无文字贴图按钮 + 网格注入"激活三件套"绑定,就绕开了 `light_text_button` 的空 binding_name 炸弹;`buildMode` 一定要 `dev` 否则产物不更新;`$pressed_button_name` 记得用变量。