@lingfan-engine/editor 0.0.0-stage → 0.1.2

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.
Files changed (79) hide show
  1. package/LICENSE +201 -0
  2. package/package.json +39 -3
  3. package/src/chapters/build.ts +180 -0
  4. package/src/chapters/index.ts +34 -0
  5. package/src/chapters/types.ts +71 -0
  6. package/src/contracts/diagnostics.ts +62 -0
  7. package/src/contracts/editing.ts +26 -0
  8. package/src/contracts/forms.ts +69 -0
  9. package/src/contracts/index.ts +22 -0
  10. package/src/contracts/texts.ts +204 -0
  11. package/src/diagnostics/analyze.ts +161 -0
  12. package/src/diagnostics/grouping.ts +164 -0
  13. package/src/diagnostics/index.ts +35 -0
  14. package/src/diagnostics/message.ts +60 -0
  15. package/src/diagnostics/refs.ts +123 -0
  16. package/src/diagnostics/symbols.ts +221 -0
  17. package/src/editing/columnGrouping.ts +331 -0
  18. package/src/editing/graphConnect.ts +88 -0
  19. package/src/editing/index.ts +72 -0
  20. package/src/editing/pointers.ts +291 -0
  21. package/src/editing/saveNormalization.ts +58 -0
  22. package/src/editing/session.ts +134 -0
  23. package/src/editing/storyOps.ts +268 -0
  24. package/src/editing/tsSourceGuard.ts +40 -0
  25. package/src/element/drag.ts +22 -0
  26. package/src/element/dragCreate.ts +60 -0
  27. package/src/element/index.ts +11 -0
  28. package/src/i18n/coverage.ts +191 -0
  29. package/src/i18n/extract.ts +38 -0
  30. package/src/i18n/index.ts +47 -0
  31. package/src/i18n/reconcile.ts +71 -0
  32. package/src/i18n/skeleton.ts +112 -0
  33. package/src/i18n/storyKeys.ts +27 -0
  34. package/src/i18n/surfaces.ts +54 -0
  35. package/src/i18n/table.ts +235 -0
  36. package/src/index.ts +342 -0
  37. package/src/layout/index.ts +14 -0
  38. package/src/layout/steps.ts +378 -0
  39. package/src/schema/catalog.ts +300 -0
  40. package/src/schema/elementForms.ts +307 -0
  41. package/src/schema/elementPalette.ts +100 -0
  42. package/src/schema/forms.ts +196 -0
  43. package/src/schema/index.ts +55 -0
  44. package/src/schema/opSchemas/commands.ts +374 -0
  45. package/src/schema/opSchemas/fragments.ts +24 -0
  46. package/src/schema/opSchemas/index.ts +16 -0
  47. package/src/schema/opSchemas/map.ts +134 -0
  48. package/src/schema/opSchemas/types.ts +42 -0
  49. package/src/schema/opSchemas/validate.ts +184 -0
  50. package/src/schema/surface.ts +101 -0
  51. package/src/schema/validation.ts +92 -0
  52. package/src/schema/walk.ts +167 -0
  53. package/src/script/expr/audit/index.ts +8 -0
  54. package/src/script/expr/audit/parser.ts +370 -0
  55. package/src/script/expr/audit/token.ts +23 -0
  56. package/src/script/expr/build.ts +203 -0
  57. package/src/script/expr/index.ts +17 -0
  58. package/src/script/expr/registry.ts +130 -0
  59. package/src/script/index.ts +168 -0
  60. package/src/script/vocabulary-gen.ts +202 -0
  61. package/src/script/vocabulary.g.ts +175 -0
  62. package/src/script/words/audio.ts +114 -0
  63. package/src/script/words/cells.ts +78 -0
  64. package/src/script/words/compile-time.ts +27 -0
  65. package/src/script/words/composite.ts +125 -0
  66. package/src/script/words/elements.ts +202 -0
  67. package/src/script/words/extension.ts +25 -0
  68. package/src/script/words/flow.ts +391 -0
  69. package/src/script/words/index.ts +23 -0
  70. package/src/script/words/interaction.ts +44 -0
  71. package/src/script/words/minigame.ts +66 -0
  72. package/src/script/words/narrative.ts +261 -0
  73. package/src/script/words/save.ts +54 -0
  74. package/src/script/words/vars.ts +155 -0
  75. package/src/script/words/video.ts +80 -0
  76. package/src/shared/guards.ts +19 -0
  77. package/src/shared/index.ts +6 -0
  78. package/src/shared/pointer.ts +23 -0
  79. package/README.md +0 -3
@@ -0,0 +1,300 @@
1
+ /**
2
+ * op 目录:op 标签/分组(含分组顺序与中文标签,单一事实源)+ 字段呈现语义(label/kind)。
3
+ * 字段的类型/必填/可选值/默认值以 opSchemas(Zod)为唯一事实源——本文件只承载
4
+ * 呈现与诊断语义元数据,字段名集合与 schema 由互锁测试锁定(schema.test.ts)。
5
+ * 字段语义以执行器为权威(packages/core/src/runtime/ops/)。
6
+ */
7
+
8
+ import type { FieldKind, OpGroup, OpMeta } from "../contracts/index.ts";
9
+
10
+ /**
11
+ * op 分组顺序(与时间线插入菜单一致)。
12
+ *
13
+ * 放在本数据叶子里:`surface`(扩展 op 归组校验)与 `elementPalette`(分组渲染)
14
+ * 都要取用,两者又分处依赖链两端。
15
+ */
16
+ export const OP_GROUP_ORDER: readonly OpGroup[] = [
17
+ "narrative",
18
+ "presentation",
19
+ "flow",
20
+ "variables",
21
+ "save",
22
+ "audio",
23
+ "video",
24
+ "minigame",
25
+ ];
26
+
27
+ /** op 分组中文标签(单一事实源) */
28
+ export const OP_GROUP_LABELS: Readonly<Record<OpGroup, string>> = {
29
+ narrative: "叙事",
30
+ presentation: "表现",
31
+ flow: "流程",
32
+ variables: "变量",
33
+ save: "存档",
34
+ audio: "音频",
35
+ video: "视频",
36
+ minigame: "小游戏",
37
+ };
38
+
39
+ export const OP_META: readonly OpMeta[] = [
40
+ { op: "say", label: "对话", group: "narrative" },
41
+ { op: "menu", label: "选项", group: "narrative" },
42
+ { op: "input", label: "输入", group: "narrative" },
43
+ { op: "notify", label: "通知", group: "narrative" },
44
+ { op: "nvl", label: "NVL 模式", group: "presentation" },
45
+ { op: "character", label: "角色定义", group: "presentation" },
46
+ { op: "uncharacter", label: "角色注销", group: "presentation" },
47
+ { op: "wait", label: "等待", group: "flow" },
48
+ { op: "pause", label: "暂停", group: "flow" },
49
+ { op: "random", label: "随机数", group: "flow" },
50
+ { op: "jump", label: "跳转", group: "flow" },
51
+ { op: "navigate", label: "导航", group: "flow" },
52
+ { op: "func", label: "函数定义", group: "flow" },
53
+ { op: "call", label: "调用函数", group: "flow" },
54
+ { op: "return", label: "函数返回", group: "flow" },
55
+ { op: "if", label: "条件分支", group: "flow" },
56
+ { op: "while", label: "循环", group: "flow" },
57
+ { op: "for", label: "遍历循环", group: "flow" },
58
+ { op: "foreach", label: "集合迭代", group: "flow" },
59
+ { op: "switch", label: "多路分支", group: "flow" },
60
+ { op: "break", label: "跳出循环", group: "flow" },
61
+ { op: "continue", label: "下一轮循环", group: "flow" },
62
+ { op: "assert", label: "断言校验", group: "flow" },
63
+ { op: "guard", label: "守卫校验", group: "flow" },
64
+ { op: "set", label: "赋值", group: "variables" },
65
+ { op: "define", label: "全局定义", group: "variables" },
66
+ { op: "let", label: "块级变量", group: "variables" },
67
+ { op: "local", label: "块级变量", group: "variables" },
68
+ { op: "undef", label: "销毁变量", group: "variables" },
69
+ { op: "array", label: "数组定义", group: "variables" },
70
+ { op: "array_push", label: "数组追加", group: "variables" },
71
+ { op: "array_pop", label: "数组弹出", group: "variables" },
72
+ { op: "dict", label: "字典定义", group: "variables" },
73
+ { op: "dict_set", label: "字典写入", group: "variables" },
74
+ { op: "save", label: "存档", group: "save" },
75
+ { op: "load", label: "读档", group: "save" },
76
+ { op: "auto_save", label: "自动存档开关", group: "save" },
77
+ { op: "save_delete", label: "删除存档", group: "save" },
78
+ { op: "bgm", label: "背景乐", group: "audio" },
79
+ { op: "stop_bgm", label: "停止背景乐", group: "audio" },
80
+ { op: "se", label: "音效", group: "audio" },
81
+ { op: "ambient", label: "环境音", group: "audio" },
82
+ { op: "stop_ambient", label: "停止环境音", group: "audio" },
83
+ { op: "voice", label: "语音", group: "audio" },
84
+ { op: "stop_voice", label: "停止语音", group: "audio" },
85
+ { op: "video", label: "视频", group: "video" },
86
+ { op: "cutscene", label: "过场", group: "video" },
87
+ { op: "seek_video", label: "视频跳转", group: "video" },
88
+ { op: "pause_video", label: "视频暂停", group: "video" },
89
+ { op: "resume_video", label: "视频恢复", group: "video" },
90
+ { op: "stop_video", label: "停止视频", group: "video" },
91
+ { op: "video_skipable", label: "视频可跳开关", group: "video" },
92
+ { op: "minigame", label: "小游戏", group: "minigame" },
93
+ { op: "interaction", label: "玩法系统接管", group: "minigame" },
94
+ // 元素系统
95
+ { op: "show", label: "显示元素", group: "presentation" },
96
+ { op: "hide", label: "隐藏元素", group: "presentation" },
97
+ { op: "background", label: "设置背景", group: "presentation" },
98
+ { op: "bg_switch", label: "切换背景", group: "presentation" },
99
+ { op: "zindex", label: "调整层级", group: "presentation" },
100
+ { op: "style", label: "元素样式", group: "presentation" },
101
+ { op: "window", label: "对话框显隐", group: "presentation" },
102
+ { op: "animate", label: "元素动画", group: "presentation" },
103
+ { op: "animate_block", label: "多属性动画", group: "presentation" },
104
+ { op: "transition", label: "转场", group: "presentation" },
105
+ { op: "shake", label: "屏幕震动", group: "presentation" },
106
+ { op: "text_typewriter", label: "打字机设置", group: "presentation" },
107
+ ];
108
+
109
+ interface FieldMeta {
110
+ label: string;
111
+ kind: FieldKind;
112
+ }
113
+
114
+ /**
115
+ * 字段元数据:键 = `<op>.<字段路径>`;嵌套项内字段以 `[]` 标注项路径
116
+ * (如 menu.options[].text)。kind 语义见 contracts/forms.ts FieldKind。
117
+ */
118
+ export const FIELD_META: Readonly<Record<string, FieldMeta>> = {
119
+ "say.text": { label: "文本", kind: "text" },
120
+ "say.speaker": { label: "说话人", kind: "text" },
121
+ // 说话人颜色**覆盖**(本句有效,优先于 character 定义的颜色);十六进制
122
+ "say.color": { label: "说话人颜色(本句)", kind: "text" },
123
+ // 本句字体覆盖(字体名 = Resources/Fonts 文件名去扩展名;优先于角色/列级字体)
124
+ "say.font": { label: "本句字体", kind: "string" },
125
+ "say.clickable": { label: "点击后继续", kind: "boolean" },
126
+ "say.noskip": { label: "禁止跳过", kind: "boolean" },
127
+ "say.instant": { label: "瞬间显示", kind: "boolean" },
128
+ "say.typewriter": { label: "打字机速度", kind: "number" },
129
+ "say.voice": { label: "本句语音", kind: "resource" },
130
+ "say.template": { label: "对话框模板", kind: "string" },
131
+ "say.z": { label: "层级(实例)", kind: "number" },
132
+ "menu.prompt": { label: "提示语", kind: "text" },
133
+ "menu.options": { label: "选项", kind: "array" },
134
+ "menu.options[].text": { label: "选项文本", kind: "text" },
135
+ "menu.options[].target": { label: "目标列", kind: "identifier" },
136
+ "menu.z": { label: "层级(实例)", kind: "number" },
137
+ "input.prompt": { label: "提示语", kind: "text" },
138
+ "input.store": { label: "存入变量", kind: "identifier" },
139
+ "input.z": { label: "层级(实例)", kind: "number" },
140
+ "notify.text": { label: "文本", kind: "text" },
141
+ "notify.type": { label: "类型", kind: "string" },
142
+ "notify.duration": { label: "时长(毫秒)", kind: "number" },
143
+ "notify.z": { label: "层级(实例)", kind: "number" },
144
+ "nvl.mode": { label: "模式", kind: "enum" },
145
+ "character.key": { label: "角色键", kind: "identifier" },
146
+ "character.name": { label: "显示名", kind: "text" },
147
+ "character.color": { label: "颜色", kind: "string" },
148
+ "character.size": { label: "字号", kind: "string" },
149
+ "character.font": { label: "字体", kind: "string" },
150
+ "character.textColor": { label: "文字颜色", kind: "string" },
151
+ "character.screen": { label: "对话框模板", kind: "string" },
152
+ "uncharacter.key": { label: "角色键", kind: "identifier" },
153
+ "wait.seconds": { label: "秒数", kind: "number" },
154
+ "wait.skipable": { label: "可跳过", kind: "boolean" },
155
+ "pause.seconds": { label: "秒数", kind: "number" },
156
+ "random.seed": { label: "种子", kind: "integer" },
157
+ "random.range": { label: "范围 [min, max]", kind: "tuple" },
158
+ "random.var": { label: "写入变量", kind: "identifier" },
159
+ "jump.target": { label: "目标列", kind: "identifier" },
160
+ "navigate.path": { label: "路径/列", kind: "identifier" },
161
+ "navigate.scene": { label: "场景列", kind: "identifier" },
162
+ "func.name": { label: "函数名", kind: "identifier" },
163
+ "func.params": { label: "形参", kind: "array" },
164
+ "func.body": { label: "函数体", kind: "body" },
165
+ "call.target": { label: "函数名", kind: "identifier" },
166
+ "call.args": { label: "实参", kind: "array" },
167
+ "call.into": { label: "结果绑定名", kind: "identifier" },
168
+ "return.value": { label: "返回值", kind: "value" },
169
+ "if.cond": { label: "条件", kind: "expression" },
170
+ "if.then": { label: "则", kind: "body" },
171
+ "if.elif": { label: "否则如果", kind: "array" },
172
+ "if.elif[].cond": { label: "条件", kind: "expression" },
173
+ "if.elif[].then": { label: "则", kind: "body" },
174
+ "if.else": { label: "否则", kind: "body" },
175
+ "while.cond": { label: "条件", kind: "expression" },
176
+ "while.body": { label: "循环体", kind: "body" },
177
+ "assert.cond": { label: "断言条件", kind: "expression" },
178
+ "assert.message": { label: "失败提示", kind: "text" },
179
+ "guard.fn": { label: "守卫名", kind: "identifier" },
180
+ "guard.args": { label: "参数", kind: "object" },
181
+ "for.var": { label: "循环变量", kind: "identifier" },
182
+ "for.in": { label: "序列表达式", kind: "expression" },
183
+ "for.body": { label: "循环体", kind: "body" },
184
+ "foreach.var": { label: "元素变量", kind: "identifier" },
185
+ "foreach.key": { label: "集合变量", kind: "identifier" },
186
+ "foreach.body": { label: "循环体", kind: "body" },
187
+ "switch.on": { label: "分支表达式", kind: "expression" },
188
+ "switch.cases": { label: "分支", kind: "array" },
189
+ "switch.cases[].value": { label: "匹配值", kind: "value" },
190
+ "switch.cases[].body": { label: "分支体", kind: "body" },
191
+ "switch.default": { label: "缺省分支", kind: "body" },
192
+ "set.key": { label: "变量名", kind: "identifier" },
193
+ "set.value": { label: "值", kind: "value" },
194
+ "define.key": { label: "变量名", kind: "identifier" },
195
+ "define.value": { label: "值", kind: "value" },
196
+ "let.key": { label: "变量名", kind: "identifier" },
197
+ "let.value": { label: "值", kind: "value" },
198
+ "local.key": { label: "变量名", kind: "identifier" },
199
+ "local.value": { label: "值", kind: "value" },
200
+ "undef.key": { label: "变量名", kind: "identifier" },
201
+ "array.key": { label: "变量名", kind: "identifier" },
202
+ "array.items": { label: "初始项", kind: "array" },
203
+ "array.once": { label: "已存在时跳过", kind: "boolean" },
204
+ "array_push.key": { label: "目标数组", kind: "identifier" },
205
+ "array_push.value": { label: "值", kind: "value" },
206
+ "array_pop.key": { label: "目标数组", kind: "identifier" },
207
+ "dict.key": { label: "变量名", kind: "identifier" },
208
+ "dict.value": { label: "初始字段", kind: "object" },
209
+ "dict.once": { label: "已存在时跳过", kind: "boolean" },
210
+ "dict_set.key": { label: "目标字典", kind: "identifier" },
211
+ "dict_set.field": { label: "字段名", kind: "identifier" },
212
+ "dict_set.value": { label: "值", kind: "value" },
213
+ "save.slot": { label: "槽位", kind: "identifier" },
214
+ "save.title": { label: "标题", kind: "text" },
215
+ "load.slot": { label: "槽位", kind: "identifier" },
216
+ "auto_save.enabled": { label: "启用", kind: "boolean" },
217
+ "save_delete.slot": { label: "槽位", kind: "identifier" },
218
+ "bgm.resource": { label: "资源", kind: "resource" },
219
+ "bgm.volume": { label: "音量", kind: "number" },
220
+ "bgm.loop": { label: "循环", kind: "boolean" },
221
+ "bgm.fade": { label: "淡入(毫秒)", kind: "number" },
222
+ "bgm.restart": { label: "回起点重播", kind: "boolean" },
223
+ "stop_bgm.fade": { label: "淡出(毫秒)", kind: "number" },
224
+ "se.resource": { label: "资源", kind: "resource" },
225
+ "se.volume": { label: "音量", kind: "number" },
226
+ "ambient.resource": { label: "资源", kind: "resource" },
227
+ "ambient.volume": { label: "音量", kind: "number" },
228
+ "ambient.loop": { label: "循环", kind: "boolean" },
229
+ "ambient.fade": { label: "淡入(毫秒)", kind: "number" },
230
+ "ambient.restart": { label: "回起点重播", kind: "boolean" },
231
+ "stop_ambient.fade": { label: "淡出(毫秒)", kind: "number" },
232
+ "voice.resource": { label: "资源", kind: "resource" },
233
+ "voice.volume": { label: "音量", kind: "number" },
234
+ "voice.auto_stop": { label: "推进即停", kind: "boolean" },
235
+ "voice.restart": { label: "回起点重播", kind: "boolean" },
236
+ "stop_voice.fade": { label: "淡出(毫秒)", kind: "number" },
237
+ "video.resource": { label: "资源", kind: "resource" },
238
+ "video.volume": { label: "音量", kind: "number" },
239
+ "video.loop": { label: "循环", kind: "boolean" },
240
+ "video.z": { label: "层级(实例)", kind: "number" },
241
+ "cutscene.resource": { label: "资源", kind: "resource" },
242
+ "cutscene.volume": { label: "音量", kind: "number" },
243
+ "cutscene.skipable": { label: "可跳过", kind: "boolean" },
244
+ "cutscene.z": { label: "层级(实例)", kind: "number" },
245
+ "seek_video.seconds": { label: "目标秒数", kind: "number" },
246
+ "video_skipable.value": { label: "启用", kind: "boolean" },
247
+ "minigame.game": { label: "游戏 ID", kind: "identifier" },
248
+ "minigame.config": { label: "配置", kind: "object" },
249
+ "minigame.on_success": { label: "成功跳转列", kind: "identifier" },
250
+ "minigame.on_fail": { label: "失败跳转列", kind: "identifier" },
251
+ "minigame.reward": { label: "奖励", kind: "array" },
252
+ "minigame.reward[].key": { label: "奖励变量", kind: "identifier" },
253
+ "minigame.reward[].value": { label: "奖励值", kind: "value" },
254
+ "minigame.z": { label: "层级(实例)", kind: "number" },
255
+ "interaction.system": { label: "玩法系统标识", kind: "identifier" },
256
+ "interaction.config": { label: "配置", kind: "object" },
257
+ "interaction.on_success": { label: "成功跳转列", kind: "identifier" },
258
+ "interaction.on_fail": { label: "失败跳转列", kind: "identifier" },
259
+ "interaction.z": { label: "层级(实例)", kind: "number" },
260
+ // —— 元素增删改 ——
261
+ "show.target": { label: "资源路径", kind: "resource" },
262
+ "show.x": { label: "X(数字或 CSS 长度)", kind: "string" },
263
+ "show.y": { label: "Y(数字或 CSS 长度)", kind: "string" },
264
+ "show.id": { label: "元素 id", kind: "identifier" },
265
+ "show.name": { label: "分组名", kind: "identifier" },
266
+ "show.background": { label: "作为背景(底层)", kind: "boolean" },
267
+ "hide.target": { label: "目标(id / 分组 / 资源)", kind: "identifier" },
268
+ "background.resource": { label: "资源", kind: "resource" },
269
+ "bg_switch.resource": { label: "资源", kind: "resource" },
270
+ "zindex.target": { label: "目标元素", kind: "identifier" },
271
+ "zindex.value": { label: "层级", kind: "number" },
272
+ "style.target": { label: "目标元素", kind: "identifier" },
273
+ "style.props": { label: "样式属性", kind: "object" },
274
+ "window.mode": { label: "模式", kind: "enum" },
275
+ // —— 帧驱动表现 ——
276
+ "animate.target": { label: "目标元素", kind: "identifier" },
277
+ "animate.property": { label: "属性名", kind: "string" },
278
+ "animate.value": { label: "目标值", kind: "number" },
279
+ "animate.duration": { label: "时长(秒)", kind: "number" },
280
+ "animate.easing": { label: "缓动", kind: "string" },
281
+ "animate_block.target": { label: "目标元素", kind: "identifier" },
282
+ "animate_block.x": { label: "X", kind: "number" },
283
+ "animate_block.y": { label: "Y", kind: "number" },
284
+ "animate_block.opacity": { label: "不透明度", kind: "number" },
285
+ "animate_block.rotation": { label: "旋转(度)", kind: "number" },
286
+ "animate_block.scale": { label: "缩放", kind: "number" },
287
+ "animate_block.duration": { label: "时长(秒)", kind: "number" },
288
+ "animate_block.easing": { label: "缓动", kind: "string" },
289
+ "transition.type": { label: "效果名", kind: "string" },
290
+ "transition.duration": { label: "时长(秒)", kind: "number" },
291
+ "shake.intensity": { label: "强度", kind: "number" },
292
+ "shake.duration": { label: "时长(秒)", kind: "number" },
293
+ "text_typewriter.enabled": { label: "启用", kind: "boolean" },
294
+ "text_typewriter.speed": { label: "速度(字符/秒)", kind: "number" },
295
+ };
296
+
297
+ /** 文本插值面(诊断扫描用):text kind 字段集合的查询入口 */
298
+ export function fieldMetaOf(key: string): FieldMeta | undefined {
299
+ return FIELD_META[key];
300
+ }
@@ -0,0 +1,307 @@
1
+ /**
2
+ * 舞台编辑:元素属性表单描述符(**契约驱动**)。
3
+ *
4
+ * 元素不是 op(无 op schema),字段面直接取自引擎元素契约(单一事实源):
5
+ * - `ELEMENT_TYPES`(36 类型)
6
+ * - `ELEMENT_ATTRIBUTES`(通用属性全集)
7
+ * - `ELEMENT_SPECIFIC_ATTRS`(分类型属性,供高亮)
8
+ *
9
+ * 产出的 `FieldDescriptor` 与 op 表单同构 → 舞台编辑可直接复用 `FieldRow`。
10
+ */
11
+ import type { FieldDescriptor, FieldKind } from "../contracts/index.ts";
12
+ import {
13
+ ELEMENT_ATTRIBUTES,
14
+ ELEMENT_SPECIFIC_ATTRS,
15
+ ELEMENT_TYPES,
16
+ } from "@lingfan-engine/core";
17
+
18
+ /** 36 类型中文标签(新建菜单与画布角标) */
19
+ const ELEMENT_LABELS: Readonly<Record<string, string>> = {
20
+ text: "文本",
21
+ dialog: "对话窗",
22
+ narrator: "旁白",
23
+ speaker: "说话人",
24
+ button: "按钮",
25
+ choice: "选项",
26
+ imagebutton: "图像按钮",
27
+ image: "图片",
28
+ background: "背景",
29
+ portrait: "立绘",
30
+ video: "视频",
31
+ panel: "面板",
32
+ frame: "框架",
33
+ window: "窗口",
34
+ dialogbox: "对话框",
35
+ choicebox: "选项框",
36
+ infobox: "信息框",
37
+ overlay: "遮罩",
38
+ popup: "浮窗",
39
+ vbox: "纵排",
40
+ hbox: "横排",
41
+ grid: "网格",
42
+ stack: "堆叠",
43
+ stackpanel: "堆叠面板",
44
+ canvas: "画布",
45
+ border: "边框",
46
+ scroll: "滚动",
47
+ scrollviewer: "滚动视图",
48
+ viewport: "视口",
49
+ bar: "进度条",
50
+ vbar: "纵向进度条",
51
+ progressbar: "进度条",
52
+ slider: "滑块",
53
+ checkbox: "复选框",
54
+ separator: "分隔线",
55
+ spacer: "留白",
56
+ };
57
+
58
+ /**
59
+ * 已声明但**当前无渲染语义**的属性(未实现属性清单:先保留解析、补实现按需)。
60
+ *
61
+ * 判定依据(代码事实):这些键只在 `contracts/element.ts` 的 `ELEMENT_ATTRIBUTES` 里声明,
62
+ * `packages/ui` **零消费**(`elementStyle` / `renderers` 都不读)——写进故事**不生效也不报错**。
63
+ *
64
+ * 处置三条:① **保留在契约里**(`ELEMENT_ATTRIBUTES` 不动,避免历史工程解析失败);
65
+ * ② **表单不下发**(`describeElement` 过滤掉,不再给作者"看起来能用"的控件);
66
+ * ③ **编辑期 warning**(诊断码 `unimplemented-element-attr`)。
67
+ *
68
+ * 与「三面对齐互锁」测试的关系:该测试独立推导"契约 − 消费者"的差集,本清单是它的**显式白名单**,
69
+ * 两边不一致即测试变红(缺了「同时看这三面」的测试,这类差集就会长期潜伏)。
70
+ */
71
+ export const UNIMPLEMENTED_ELEMENT_ATTRS: ReadonlySet<string> = new Set([
72
+ "valign",
73
+ "xalign",
74
+ "yalign",
75
+ // 例外:order 不是死属性——引擎装载期在用(z 兜底:
76
+ // packages/core/src/data/element.ts:157、runtime/internal/frame-stack.ts:62 消费)。
77
+ // 保留本条仅为维持现状行为(编辑器表单不下发 + unimplemented-element-attr warning +
78
+ // 互锁白名单 attr-alignment.test.ts),即「编辑器表单不暴露但引擎装载在用」。
79
+ "order",
80
+ "xoffset",
81
+ "yoffset",
82
+ "xanchor",
83
+ "yanchor",
84
+ // 互锁建立时反向逮出的一条:`image`/`imagebutton`/`portrait` 有表单
85
+ // (「拉伸方式」)但 `packages/ui` 零消费——这类差集须从契约全集反推
86
+ // 才能逮出,互锁测试从此守着这个洞。
87
+ "stretch",
88
+ ]);
89
+
90
+ /** 属性 → 呈现语义(未列出者回退 `string`) */
91
+ const ELEMENT_FIELD_META: Readonly<
92
+ Record<string, { label: string; kind: FieldKind }>
93
+ > = {
94
+ id: { label: "元素 id", kind: "identifier" },
95
+ name: { label: "分组名", kind: "identifier" },
96
+ class: { label: "样式类", kind: "string" },
97
+ style: { label: "样式类(别名)", kind: "string" },
98
+ source: { label: "资源路径", kind: "resource" },
99
+ src: { label: "资源路径", kind: "resource" },
100
+ path: { label: "资源路径", kind: "resource" },
101
+ text: { label: "文本内容", kind: "text" },
102
+ align: { label: "对齐(别名)", kind: "string" },
103
+ halign: { label: "水平对齐", kind: "string" },
104
+ valign: { label: "垂直对齐", kind: "string" },
105
+ xalign: { label: "水平锚点", kind: "string" },
106
+ yalign: { label: "垂直锚点", kind: "string" },
107
+ // 「数字或 CSS 长度」一律 kind:value —— 纯数字须还原为数字,运行时 len() 才补 px;
108
+ // 退化成字符串 "120" 会被 CSS 判为非法并静默丢弃(元素停在 0,0)
109
+ x: { label: "X(数字或 CSS 长度)", kind: "value" },
110
+ y: { label: "Y(数字或 CSS 长度)", kind: "value" },
111
+ xoffset: { label: "X 偏移", kind: "value" },
112
+ yoffset: { label: "Y 偏移", kind: "value" },
113
+ xanchor: { label: "X 锚点", kind: "string" },
114
+ yanchor: { label: "Y 锚点", kind: "string" },
115
+ width: { label: "宽度", kind: "value" },
116
+ height: { label: "高度", kind: "value" },
117
+ minWidth: { label: "最小宽度", kind: "value" },
118
+ minHeight: { label: "最小高度", kind: "value" },
119
+ maxWidth: { label: "最大宽度", kind: "value" },
120
+ maxHeight: { label: "最大高度", kind: "value" },
121
+ margin: { label: "外边距", kind: "value" },
122
+ padding: { label: "内边距", kind: "value" },
123
+ right: { label: "右偏移", kind: "value" },
124
+ bottom: { label: "下偏移", kind: "value" },
125
+ fontSize: { label: "字号", kind: "value" },
126
+ size: { label: "字号(文本类别名)", kind: "value" },
127
+ font: { label: "字体", kind: "string" },
128
+ color: { label: "颜色", kind: "string" },
129
+ fontColor: { label: "文字颜色", kind: "string" },
130
+ textColor: { label: "文字颜色", kind: "string" },
131
+ textAlign: { label: "文本对齐", kind: "string" },
132
+ order: { label: "排列序", kind: "number" },
133
+ zindex: { label: "层级", kind: "number" },
134
+ opacity: { label: "不透明度", kind: "number" },
135
+ visible: { label: "可见", kind: "boolean" },
136
+ enabled: { label: "启用", kind: "boolean" },
137
+ // 布尔或表达式(`{player.gold < 10}`,点击时刻求值)——故用 value:控件接受两种形态
138
+ disabled: { label: "禁用(布尔或表达式)", kind: "value" },
139
+ clipToBounds: { label: "裁剪溢出", kind: "boolean" },
140
+ cursor: { label: "光标", kind: "string" },
141
+ rotation: { label: "旋转(度)", kind: "number" },
142
+ scale: { label: "缩放", kind: "number" },
143
+ scaleX: { label: "X 缩放", kind: "number" },
144
+ scaleY: { label: "Y 缩放", kind: "number" },
145
+ cornerRadius: { label: "圆角", kind: "value" },
146
+ borderBrush: { label: "边框色", kind: "string" },
147
+ borderColor: { label: "边框色", kind: "string" },
148
+ borderThickness: { label: "边框宽度", kind: "value" },
149
+ spacing: { label: "间距", kind: "number" },
150
+ direction: { label: "方向(vertical/horizontal)", kind: "string" },
151
+ // Grid 轨道:数字 → repeat(n, 1fr)(renderers 的 toTrack),故与长度类同用 value
152
+ columns: { label: "列(数量或 CSS 轨道)", kind: "value" },
153
+ rows: { label: "行(数量或 CSS 轨道)", kind: "value" },
154
+ col: { label: "Grid 列(0 基)", kind: "number" },
155
+ row: { label: "Grid 行(0 基)", kind: "number" },
156
+ colspan: { label: "跨列", kind: "number" },
157
+ rowspan: { label: "跨行", kind: "number" },
158
+ min: { label: "最小值", kind: "number" },
159
+ max: { label: "最大值", kind: "number" },
160
+ orientation: { label: "方向", kind: "string" },
161
+ checked: { label: "已选中", kind: "boolean" },
162
+ stretch: { label: "拉伸方式", kind: "string" },
163
+ scroll_h: { label: "允许横向滚动", kind: "boolean" },
164
+ scroll_v: { label: "允许纵向滚动", kind: "boolean" },
165
+ nav: { label: "跳转列", kind: "identifier" },
166
+ cmd: { label: "宿主命令", kind: "identifier" },
167
+ value: { label: "命令参数", kind: "value" },
168
+ // `ops` = 点击动作序列(一串 op)。用 `body` 形态:与 if.then/while.body 同一控件,
169
+ // 递归复用命令面板(插入/删除/上下移/逐字段编辑),零新控件。
170
+ ops: { label: "点击动作序列", kind: "body" },
171
+ hover_source: { label: "悬停资源", kind: "resource" },
172
+ hover_color: { label: "悬停颜色", kind: "string" },
173
+ hover_opacity: { label: "悬停不透明度", kind: "number" },
174
+ selected_source: { label: "选中资源", kind: "resource" },
175
+ selected_color: { label: "选中颜色", kind: "string" },
176
+ disabled_source: { label: "禁用资源", kind: "resource" },
177
+ disabled_color: { label: "禁用颜色", kind: "string" },
178
+ disabled_opacity: { label: "禁用不透明度", kind: "number" },
179
+ };
180
+
181
+ /** 表单字段优先序(结构 → 内容 → 布局 → 外观 → 交互),未列出的属性按契约序追加 */
182
+ const FIELD_ORDER: readonly string[] = [
183
+ "id",
184
+ "name",
185
+ "text",
186
+ "source",
187
+ "src",
188
+ "path",
189
+ "x",
190
+ "y",
191
+ "width",
192
+ "height",
193
+ "zindex",
194
+ "opacity",
195
+ "visible",
196
+ "color",
197
+ "fontSize",
198
+ "font",
199
+ "size",
200
+ "halign",
201
+ "valign",
202
+ "align",
203
+ "textAlign",
204
+ "rotation",
205
+ "scale",
206
+ "scaleX",
207
+ "scaleY",
208
+ "margin",
209
+ "padding",
210
+ "xoffset",
211
+ "yoffset",
212
+ "xanchor",
213
+ "yanchor",
214
+ "right",
215
+ "bottom",
216
+ "minWidth",
217
+ "minHeight",
218
+ "maxWidth",
219
+ "maxHeight",
220
+ "cornerRadius",
221
+ "borderColor",
222
+ "borderThickness",
223
+ "direction",
224
+ "spacing",
225
+ "columns",
226
+ "rows",
227
+ "col",
228
+ "row",
229
+ "colspan",
230
+ "rowspan",
231
+ "min",
232
+ "max",
233
+ "orientation",
234
+ "checked",
235
+ "stretch",
236
+ "scroll_h",
237
+ "scroll_v",
238
+ "nav",
239
+ "ops",
240
+ "cmd",
241
+ "value",
242
+ "hover_source",
243
+ "hover_color",
244
+ "hover_opacity",
245
+ "selected_source",
246
+ "selected_color",
247
+ "disabled_source",
248
+ "disabled_color",
249
+ "disabled_opacity",
250
+ "disabled",
251
+ "enabled",
252
+ "clipToBounds",
253
+ "cursor",
254
+ "class",
255
+ "style",
256
+ "order",
257
+ ];
258
+
259
+ export interface ElementFormDescriptor {
260
+ type: string;
261
+ label: string;
262
+ fields: FieldDescriptor[];
263
+ }
264
+
265
+ /** 元素类型判定 + 表单描述符(未知类型 = undefined,调用方 fail-closed) */
266
+ export function describeElement(
267
+ type: string,
268
+ ): ElementFormDescriptor | undefined {
269
+ if (!(ELEMENT_TYPES as readonly string[]).includes(type)) return undefined;
270
+ // 不下发「已声明但无渲染语义」的属性(作者填了不生效 → 表单一律不给)
271
+ const allowed = new Set<string>(
272
+ [...ELEMENT_ATTRIBUTES, "id", "name"].filter(
273
+ (key) => !UNIMPLEMENTED_ELEMENT_ATTRS.has(key),
274
+ ),
275
+ );
276
+ const ordered = [
277
+ ...FIELD_ORDER.filter((key) => allowed.has(key)),
278
+ ...[...allowed].filter((key) => !FIELD_ORDER.includes(key)),
279
+ ];
280
+ return {
281
+ type,
282
+ label: ELEMENT_LABELS[type] ?? type,
283
+ fields: ordered.map((key) => ({
284
+ key,
285
+ label: ELEMENT_FIELD_META[key]?.label ?? key,
286
+ kind: ELEMENT_FIELD_META[key]?.kind ?? "string",
287
+ // 元素属性一律可选(`type` 是结构字段,不在表单里编辑)
288
+ required: false,
289
+ hasDefault: false,
290
+ })),
291
+ };
292
+ }
293
+
294
+ /** 36 类型(新建元素菜单) */
295
+ export function listElementTypes(): readonly string[] {
296
+ return ELEMENT_TYPES;
297
+ }
298
+
299
+ /** 类型中文标签 */
300
+ export function elementLabel(type: string): string {
301
+ return ELEMENT_LABELS[type] ?? type;
302
+ }
303
+
304
+ /** 该类型的专属属性(表单高亮用;通用属性表已含并集) */
305
+ export function specificAttrsOf(type: string): readonly string[] {
306
+ return ELEMENT_SPECIFIC_ATTRS[type] ?? [];
307
+ }