Files
youle_cocos/docs/superpowers/specs/2026-08-28-legacy-ui-migration-design.md
T
joywayerandClaude Opus 5 02dc5d51ca chore(spec): 综合清理 + legacy-layer 迁移 spec/plan/data
主要改动:
- 切到 funplay-cocos-mcp v0.5.1 (用户级配置, 项目级 .mcp.json 删除)
- 仓库文档/CLAUDE.md/.gitignore 等清理过时 cocos-mcp-server 引用
- memory 文件同步: cocos-mcp-setup/path/blocker/spriteframe-uuid/prefab-persist 等加 funplay 实测警告
- memory 新建 funplay-cocos-mcp-pending-verification.md (后已被实测覆盖)
- spec/plan/data:
  - docs/superpowers/specs/2026-09-02-legacy-layer-migration-design.md
  - docs/superpowers/plans/2026-09-02-legacy-layer-migration.md
  - docs/superpowers/data/layer-spirit-summary.json
- YouleNexus: profiles.ts / defaults.ts / PlayerInfoView.prefab / scene 改动

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 07:36:54 +08:00

409 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 旧 gameabc UI 数据 → Cocos 平台界面:迁移与分辨率适配 设计
> 状态:已与用户确认(brainstorming 通过),待转 writing-plans。
> 适用工程:`cocoscreator_projects/YouleNexus`(框架真源)。
> 数据源:`projects/Game_Surface_3/output/gameabc_*.json`(旧 H5 工程的 UI 导出数据)。
> 关联:`2026-08-27-ui-asset-and-skin-design.md`(资源归属 §2 / 图集划分 / override 白名单 / theme §4);
> `2026-06-28-cocos-framework-design.md` §5(换肤)。
> **本 spec 只覆盖子系统 A**,B(引擎机制映射)与 C(平台逻辑移植)另立,见 §0.1。
---
## 0. 目标与背景
### 0.1 范围:这是三个子系统中的第一个
把「旧平台 UI 搬到 Cocos」拆成三件互相独立的事,本 spec 只做 A:
| | 子系统 | 产出 | 依赖 |
|---|---|---|---|
| **A** | **UI 数据迁移**(本 spec) | 991 对象 / 55 界面 → 中间描述 → prefab + 1306 张散图 | 无 |
| **B** | 引擎机制映射 | ~10 个 `ifast_*` API + `set_self`/`get_self` **属性号对照表** → Cocos 等价物 | 需逆向 `js/gameabc.min.js`(186KB) |
| **C** | 平台逻辑移植 | `11_GameUI.js`(9862 行)等 → TypeScript | **A 与 B 都要先有** |
A 与 B 互不依赖、可并行;C 必须等两者。
> **B 的紧迫性**:平台层有 `set_self` **1773 处** + `get_self` **447 处**调用,全部使用数字属性号(37/7/18/20/43…)。若属性号解不出来,C 就只能**重写**而非**移植**,工作量差一个量级。建议 A 开工后尽快并行启动 B。
### 0.2 决策依据(实测数据)
以下全部实测自 `projects/Game_Surface_3`,2026-08-28。**记录在此,便于日后回看每个取舍的依据。**
**布局数据规模**
- `gameabc_Object.json`:**991 个对象**,`ObjectType` 仅两种——`2` = 图片精灵(782)、`4` = 文字(209)
- `gameabc_Layer.json`:**55 个 Layer = 55 个界面**(`Logo_Layer` / `Login_Layer` / `MainMenu_Layer` / `Player_Head_Score_Layer` / `inputPannel` …)
- `gameabc_GroupList.json`:**80 个组**,覆盖 **990/991** 个对象(仅 1 个未分组)
- **109 个对象**使用九宫格(`L9`/`T9`/`R9`/`B9` 非零)
- **52 个对象**为满屏尺寸(≥1280×720),其中 **40 个是所在层的首个对象**
- **222 个对象**坐标超出画布边界;`Left` 范围 **−286 ~ 1501**、`Top` 范围 **−318 ~ 1206**
**图片资源**
- `gameabc_Image.json`:**439 张图**,其中 **139 张多帧(31.7%)**
- 多帧图全部为规整网格:**`w × h = frame_all`,139 张零例外**
- 切分后总计 **1006 帧 + 300 张单帧图 = 1306 张散图**
- **78/139** 张多帧图被多个对象共用,最多一张被 **13 个对象**共用
- **389 个对象**引用多帧图,其中 `FrameIndex = 0` 的仅 **2 个**
**坐标系与适配(现状)**
- 旧引擎原点**左上**、Y 向下、**精灵锚点左上**,`position = (Left, Top)`,设计分辨率 **1280×720**
- `OriginID` / `Parent` **全为 0**,`OriginPos` / `SelfPos` **全为 1** → **零相对锚定数据**
- 引擎(`js/gameabc.min.js`)用 `innerWidth`/`innerHeight` + `scale` + `onresize` 做**整画布等比缩放**,靠 letterbox 适配
**文字**
- 209 个 Type4 对象**全部没有 `ImageFileID`、`TextFrames` 为空** → 纯动态文本占位
- 样式在代码里:`Utl.setFontColor(spid, value)`(`08_Utl_Output.js:824`),引擎用 canvas `fillText`
- → **转换只能得到几何信息,字体/字号/颜色须由 `theme.fonts` 提供**
**运行时动态精灵(B 的线索,A 需为其保留信息)**
- `ifast_addtospritefromspritecopy(父精灵ID, 模板精灵ID, x, y, tag)` —— **88 处调用**
- `ifast_dllpritefromspritecopy` —— **74 处调用**
- → 典型「克隆模板 → 用完删除」的列表/对象池模式;**991 个对象中有一部分是模板、一部分是容器**
### 0.3 帧序:1 基 + 行优先(实证结论)
```
列 = (FrameIndex − 1) % w
行 = ⌊(FrameIndex − 1) / w⌋
源矩形 = (列 × w1, 行 × h1, w1, h1)
```
**验证方法**(记录在此,因为这是最容易搞错且后果最广的一处):`00014.png` 为 450×280、网格 3×4,被 11 个按钮共用。直接读取该 PNG,其网格内容为:
```
我的 福利 任务
战绩 背包 仓库
吐槽 财富榜 邀请码
通知 设置 (空)
```
对照对象的 `FrameIndex`:通知=10、设置=11、战绩=4、背包=5、仓库=6、任务=3。按「1 基 + 行优先」换算,六项**全部精确命中**;列优先与 0 基行优先均对不上。用户随后独立确认了该结论。
`FrameIndex = 0` 的 2 个对象按帧 1 处理。
> **一处被推翻的统计**:早期曾得出「1306 帧中 674 帧(51.6%)从未被引用」并险些据此裁剪资源。该数字**错误且不得使用**,两个原因:① 帧号 1 基,统计时把下标 0 当成了有效帧;② 平台层有 `set_self` 1773 处调用,**帧是运行时切换的**,静态数据里的 `FrameIndex` 只是初始帧。按钮的按下态/选中态就在那些「未引用」帧里。**任何帧都不得因静态未引用而删除。**
### 0.4 已确认决策
- **设计分辨率设为 1600×720(20:9 主流移动端比例)+ `fitHeight`** —— 991 个对象的 X/Y **全部 1:1 迁移**,不做任何坐标缩放。旧引擎的 1280×720 降格为**内容区基准**(`DESIGN_WIDTH`),只用于坐标换算、三区锚点推断、满屏/越界判断;设计分辨率画布宽度是**另一个独立常量**(`CANVAS_WIDTH = 1600`),落地时据此设定 Layer/Group 根节点尺寸。20:9 实机 1:1 无缩放,16:9 老设备由 `fitHeight` 收缩画布、Widget 弹性贴边自动适配。
- **多帧图切成散图**(1306 张)—— 多帧网格是旧引擎缺图集能力的手工替代品,Cocos 的 Auto Atlas 在构建期自动打包;照搬它等于把引擎缺陷当成新框架的约定。
- **同组对象挂同一父节点** —— `GroupList` 覆盖 990/991,是旧引擎用来做整体操作的机制,语义等价于父节点。
- **锚点按中心点三区自动推断**,仅水平方向(§2.3)。
- **背景四边拉伸**,美术后续手动更换正确尺寸的图片。
- **全部 55 个界面转进框架**(含牌桌内的平台部分)。
- **落地走「中间产物 + 编辑器批量」**(§4.1),不直接生成 `.prefab` 文件。
### 0.5 红线
- **绝不手写 `.prefab` / `.meta` / `.scene`**(CLAUDE.md)。序列化资源只能由编辑器经官方 API 产生。这一条直接否决了「Node 脚本直接吐 prefab」这一最直觉的做法(§4.1)。
- **服务器零改动**:本 spec 不触及协议。
- **数据源权威唯一、下游不兜底**:中间描述是转换的唯一真相;转换失败必须显式报错,不得静默产出残缺节点。
- **不得因静态未引用而裁剪帧**(§0.3)。
### 0.6 方法论教训(写给后续维护者)
本 spec 的调研过程中**两次**因看错字段而误判「数据不存在」:
1. 查 `OriginID`/`Parent` 全为 0 → 结论「没有层级信息」。**错**:层级语义在 `GroupList` 里。
2. 统计 `FrameIndex` 引用 → 结论「51.6% 帧是死重量」。**错**:帧号 1 基,且帧在运行时切换。
**教训**:在 gameabc 这类自研引擎的数据上,「某字段全为空」**不等于**「该语义不存在」——它可能编码在另一个字段、另一个文件,或根本在代码里。下结论前先把 `output/` 下**全部** JSON 与相关引擎 API 都看一遍。
---
## 1. 中间描述的数据模型
### 1.1 形态:一层一个 JSON + 一份 manifest
```
tools/ui-migration/intermediate/
├─ manifest.json # 55 界面索引 + ObjectID 全局映射 + 转换器版本
├─ 002-Login_Layer.json
├─ 004-MainMenu_Layer.json
└─ …(共 55 个)
```
一层一文件:**局部重跑与人工校对的单位就是界面**。改了锚点规则只想重转登录页,不该动其余 54 个。
### 1.2 节点模型
```jsonc
{
"id": 254,
"name": "微信登录按钮", // ObjectName 原样保留(中文名信息量大)
"kind": "sprite", // ObjectType 2→sprite, 4→label
"position": { "x": -245, "y": 125 }, // 已转为 Cocos 坐标系(§2.2)
"size": { "width": 150, "height": 70 },
"siblingIndex": 7, // 来自 IndexOfLayer,决定 z 序
"widget": { "horizontal": "center", "horizontalCenter": -245 }, // §2.3
"sprite": {
"frame": "atlas-login/00014_05.png", // 当前帧的逻辑路径(§3)
"frameSet": { // 供 FrameSet 组件(§3.4)
"prefix": "atlas-login/00014", // 帧文件名前缀
"count": 12, // 帧总数,索引 1..count
"pad": 2 // 补零位数,用于拼出 00014_01 … 00014_12
},
"type": "sliced", // L9/T9/R9/B9 任一非零 → sliced
"slice": { "left": 12, "right": 12, "top": 8, "bottom": 8 }
},
"flags": {
"stretch": false, // 背景类,四边拉伸(§2.4)
"offscreen": false, // 坐标超出画布(§2.5)
"animation": false // FrameStyle=1,整图动画(§3.5)
},
"legacy": { // 原始信息,不参与渲染,但**承重**(§1.3)
"objectId": 254,
"imageFileId": 14, "frameIndex": 5, "frameStyle": 0,
"groupId": 3, "voiceFileId": 0, "timerInterval": 0,
"left": 320, "top": 200, // 原始左上坐标,供回溯对照
"events": ["mousedown", "mouseup"]
// 校验:x = 320 + 150/2 − 640 = −245;y = 360 − 200 − 70/2 = 125
// cx = 320 + 75 = 395 ∈ [320, 960] → 水平居中,horizontalCenter = 395 − 640 = −245
}
}
```
`kind: "label"` 的节点只有 `position` / `size` / `widget` / `legacy`,**没有样式字段**——依据 §0.2,样式在旧代码里,数据中不存在。字体字号颜色由 `theme.fonts` 提供(见 `2026-08-27-ui-asset-and-skin-design.md` §4)。
### 1.3 `legacy.objectId` 是承重字段,不是纪念品
旧平台逻辑通过 **`set_self(精灵ID, 属性号, …)`** 操作 UI,全平台层 **1773 处**调用。**子系统 C 移植这些逻辑时,必须能由 ObjectID 定位到 Cocos 节点**——丢掉这个映射,那 1773 处调用就没有着落,C 会从「移植」退化成「重写」。
因此:
- 每个节点保留完整 `legacy` 块
- `manifest.json` 额外维护一份全局索引:**ObjectID → { 界面, 节点路径 }**
同理保留 `groupId`(B 需要它理解容器语义)与 `events`(C 需要它接事件)。
### 1.4 层级信息
```jsonc
{
"layerId": 2,
"name": "Login_Layer",
"bucket": "login", // login | hall | room | common | unassigned(§3.3)
"backgroundNodeId": 2, // 「层内首个 + 满屏」识别(40/52 命中)
"groups": [ // 来自 GroupList,同组挂同一父节点(§2.1)
{ "groupId": 2, "nodeIds": [2, 405, 285, /* … */] }
],
"nodes": [ /* 按 siblingIndex 排序 */ ]
}
```
### 1.5 本节有意不做的事
- **不推断按钮**。`Event` 含 `mousedown` 的对象包括背景图(对象 1 即是),据此判断交互性不可靠。事件原样存入 `legacy`,留给 C。
- **不识别模板/容器**。§0.2 表明存在运行时克隆的模板与容器,但要认出「哪个 ObjectID 是 `ConstVal.myRoomList.bgSp`」需要读平台层代码——那是 B/C 的范围。A 只需**忠实保留 ObjectID**,使后续可交叉引用。
---
## 2. 坐标与锚点转换规则
### 2.1 group → 父节点:父节点必须是满屏容器
子节点坐标相对父节点,而旧数据全是绝对坐标。若 group 父节点用包围盒或零尺寸,两件事会同时出问题:子坐标要重算,且 **子节点的 Widget 会锚到组边界而非屏幕边界,适配立刻失效**。
解法是让父节点**不产生坐标偏移、且与屏幕同尺寸**:
```
Layer 根节点 1600×720(设计分辨率画布),Widget 四边拉伸
└─ Group 节点 1600×720,Widget 四边拉伸,position (0,0)
└─ 对象节点 §2.2 转换后的绝对坐标(以 1280 内容区基准换算)
```
于是子对象坐标 = 转换后的绝对坐标(无需再减父偏移),其 Widget 锚定的满屏父节点等价于锚定屏幕边界。
### 2.2 坐标系转换
节点锚点采用 **Cocos 惯例 `(0.5, 0.5)`**:
```
x = Left + Width / 2 − 640
y = 360 − Top − Height / 2
```
其中 `640`/`360` 是**内容区基准 1280×720 的一半**(`DESIGN_WIDTH / 2`),不是设计分辨率画布 1600 的一半。对象中心 `x = 0` 落在内容区中心,而 Layer/Group 根节点是 1600×720,两者中心重合于屏幕中心——所以居中对象天然居中,靠边对象再由 Widget 贴到画布边缘,无需为画布变宽重算坐标。
**为何不用 `(0, 1)`(左上)**:那样公式更简(`x = Left − 640`、`y = 360 − Top`)且与旧引擎语义一一对应,但用户明确会**在编辑器里人工审核并修改节点树**,而 Cocos 的编辑器、对齐工具、Widget 全部假定锚点居中。转换公式复杂一点是转换器一次性的成本,人工编辑的别扭是长期成本。
原始 `Left`/`Top` 存入 `legacy`,回溯对照随时可查。
### 2.3 锚点推断:只需要水平方向
**`fitHeight` 下垂直方向永远精确撑满、不会溢出,因此不需要任何垂直适配**——垂直用固定位置即可。这是设计分辨率高度保持 720(与内容区基准高度一致)带来的实质简化。
水平方向按对象中心点 `cx = Left + Width / 2` 三区推断:
| 条件 | 锚定 | Widget 值 |
|---|---|---|
| `cx < 320` | 靠左 | `left = Left` |
| `cx > 960` | 靠右 | `right = 1280 − (Left + Width)` |
| 其余 | 水平居中 | `horizontalCenter = cx − 640` |
分界取**内容区基准 1280** 的四等分点(与设计分辨率画布 1600 无关;`cx`/`Left`/`right` 里的 `1280`/`640` 均指内容区基准)。这是**机械规则,不追求完美**——用户已确认转换后会逐屏人工审核修正,规则只需「大部分对、且错了容易看出」。
### 2.4 背景:四边拉伸
识别规则:**层内首个 + 满屏尺寸**(实测 40/52 命中)。此类节点 Widget 四边置 0 全拉伸,中间描述标 `flags.stretch = true`。
另外 12 个满屏但非层内首个的(如「创建房间遮罩」)同样四边拉伸——它们本就是覆盖全屏的遮罩,行为一致。
转换器**不做任何图片处理**:美术后续会手动更换为正确尺寸的图片。
### 2.5 屏外对象
222 个对象坐标超出画布(`Left −286 ~ 1501`、`Top −318 ~ 1206`),多为动画入场的暂存位。
处理:**坐标原样转换,不裁剪、不钳制**。三区规则照常适用(`cx` 为负自然落入「靠左」)。额外标 `flags.offscreen = true`,供人工审核时快速筛出——它们最可能需要手工确认锚点是否合理。
---
## 3. 多帧图切分与命名约定
### 3.1 切分规则
见 §0.3 的公式与实证。139 张多帧图全部满足 `w × h = frame_all`,切分是纯机械操作。
### 3.2 命名:保持 1 基,补零对齐字典序
```
00014.png (3×4) → 00014_01.png … 00014_12.png
00019.png (13×1) → 00019_01.png … 00019_13.png
```
**用 1 基而非 0 基**,使文件名与 `FrameIndex` 直接对齐:C 移植时代码里的 `set_self(spid, 帧属性号, 7)` 能一眼对上 `_07`,无需心算减一。这类 off-by-one 是最易反复踩的坑(§0.3 已经踩过一次统计口径)。
补零位数按 `frame_all` 决定,保证**字典序 = 帧序**(否则 `_10` 会排在 `_2` 前,图集打包与人工浏览都会乱)。
### 3.3 图集归属
每张图按**使用它的对象所在层**决定 bucket;跨多个 bucket 的图归 `atlas-common`。
层 → bucket 用实测出的 **LayerID 段位规律** + 可覆盖的显式表:
| LayerID 段 | bucket | 典型层 |
|---|---|---|
| 1–29 | `login` / `hall` | Logo/Login/Auth/Bind → login;MainMenu/Pay/Task/rankList/wareHouse → hall |
| 50 / 202 / 4xx | `room` | MainScene / Player_Head_Score / Chat / Interact / btnPannel |
| 6xx | `common` | inputPannel / Tips / Loading / Reconnect / Kick |
命名笼统的层(`Layer6` / `Layer13` / `Layer403` / `Layer411` / `Layer601` / `Layer603` / `Layer618`)标 `bucket: "unassigned"`,**不猜**,由人工归位。转换器遇到 `unassigned` 应告警而非静默归入 common。
### 3.4 运行时切帧:`FrameSet` 组件
每个引用多帧图的节点挂一个 `FrameSet` 组件,持有该图**全部帧**的 SpriteFrame 数组,索引与 `FrameIndex` 同为 1 基:
```ts
frameSet.setFrame(7); // ← C 移植时对应 set_self(spid, 帧属性号, 7)
```
副作用是必要的:全部帧都被数组引用 → **都会进图集、不会被依赖树裁掉**,符合 §0.3「任何帧都不得因静态未引用而删除」。
### 3.5 不切的情况
- **300 张单帧图**原样保留,仅按 bucket 分目录
- **`FrameStyle = 1` 的 54 个对象**(整图动画)照常切帧,节点额外标 `flags.animation = true`,供后续做逐帧动画识别
### 3.6 物理组织与命名保留
```
framework/ui/atlas-hall/00014_01.png … 00014_12.png
framework/ui/atlas-login/00006_01.png … 00006_05.png
```
**沿用原编号 + 帧号,不重命名为语义名**。两个理由:① 原编号是与 `gameabc_Image.json` 对照回溯的唯一线索;② 实测证明**对象名与美术内容会脱节**(对象叫「更多」,图上是「我的」;对象叫「分享」,图上是「福利」),语义命名会把错误信息固化进文件名。
---
## 4. 落地管线与验证
### 4.1 管线形态
```
gameabc_*.json ──[Node 转换器]──► 中间描述 55 份 + 散图 1306 张
│
▼
[编辑器侧批量落地]
│
▼
framework/ui/*.prefab(55 个)
```
**Node 侧**为纯函数:读 JSON、算坐标、推锚点、切帧、分 bucket。可 TDD,沿用既有工具链约定(`node:test`、零第三方依赖、显式路径提交)。
**编辑器侧**先用 MCP 跑通**一个**界面(建议 `Login_Layer`,18 个对象,规模合适且链路完整),确认节点树、Widget、SpriteFrame 引用无误,**再决定**是否值得写编辑器扩展批量跑完其余 54 个。不预先写扩展——先证明单条链路成立。
**为何不直接用 Node 生成 `.prefab`**:那要手写 UUID 引用的序列化格式,直接违反 §0.5 红线;991 个对象 × 55 个 prefab,格式错一点就是「资源导入失败」且只能人工恢复。
### 4.2 验证:分四层
991 个对象、1306 张图无法靠肉眼验证,故分层且尽量自动:
**① 几何与规则层(自动,TDD)**
坐标换算、三区锚点推断、bucket 归属——纯函数,用已知样例断言(如「通知」按钮的 `Left`/`Top` → 期望的 Cocos 坐标与 Widget 值写死在测试中)。
**② 切图正确性(自动,且是完全验证)**
切分是**可逆**操作:把切出的帧按原网格重组,必须与原图**逐字节相同**。
```
切分(原图) → N 帧 → 重组(N 帧) ≡ 原图(逐字节)
```
对全部 139 张多帧图跑重组校验,即**对全部 1006 帧的完全验证**,成本近零。行优先/1 基这类错误会被当场抓住——比靠读图判断可靠得多。
**③ prefab 结构(自动)**
落地后回读 prefab:节点数、层级归属(group → 父节点)、z 序、Widget 参数,与中间描述逐项比对,差异即报错。
**④ 视觉验证(半自动,抽样)**
用 MCP `cocos_capture` 截图,与原项目同界面截图并排比对。有 ①②③ 兜底后,这一层只需回答「规则本身定得对不对」(某按钮该锚右却锚了中),而非「转换器有没有 bug」。
### 4.3 与已交付工具链的衔接
切出的 1306 张图落进 `framework/ui/atlas-*`,正是 `2026-08-27-ui-asset-and-skin-design.md` §3.1 定义的**可覆盖白名单那 8 个目录**。因此:
- `check-skin` 的「框架可覆盖资源」将从**当前 0 个变为 1306 个**——换肤机制首次有真实对象
- 该 spec §6 中三条一直 ⏸ 的承重假设(Auto Atlas 构建期打包、meta 尺寸行为、Spine)首次有真实素材可验证
**A 落地时应顺手复测这些假设**,而非任其继续悬挂。
### 4.4 幂等与可重跑
- 中间描述是纯函数产物,转换器可任意重跑
- prefab 落地以**层为单位**,支持局部重跑(改了某界面的规则只重落该层)
- 散图切分同样幂等;重跑前需清理旧产物,避免残留帧文件混入图集
---
## 5. 待验证项
| # | 待验证 | 验证方法 | 失败后的备选 |
|---|---|---|---|
| 1 | Auto Atlas 尺寸上限:1306 张图分 4 bucket,某 bucket 可能超单张图集 2048×2048 | 落地后构建,检查图集张数与尺寸 | 按加载时机进一步细分 bucket(如 `atlas-hall-1/2`) |
| 2 | 包体:1306 张图全部进每个子游戏包 | 第一次真实构建后量实际包体 | 评估是否将低频界面(如 `Help`/`Protol`)转为远程 bundle |
| 3 | MCP 批量建节点的可行性与耗时(991 节点) | 实测(2026-08-29):`Login_Layer` 18 节点落地共 **76 次 MCP 调用、约 20 分钟**。`builder.build` 1 次可建整棵树,但 cc.Widget「flag 使能即采纳当前 margin」会按瞬时尺寸污染 margin,需**逐节点 `set_property` 修正**(约 22 次、每次 ~3.5s)。按 991 节点外推仅 margin 修正即需 ~1000 次调用(≈58 分钟纯往返)。**结论:逐节点 MCP 不可行** | 已确认:修 builder 的 Widget 赋值时序(margin 在最终尺寸后设)或写编辑器扩展批量修正,二者取一 |
| 4 | 三区锚点规则的实际命中率 | 抽样 5 个界面视觉比对 | 调整分界阈值,或改为逐元素人工标注 |
> **margin 污染的事后修复方法(2026-08-30 实测确认)**:cc.Widget 四边拉伸下 `contentSize` 是**派生值**(`= 父尺寸 − left − right`),不是自由变量。修正时**必须 `set_property` 设公开属性 `left`/`right`**(触发 setter 正向对齐,`contentSize` 自动跟随);**切勿 set `_contentSize`**——那会触发 Widget 反算,把刚设好的 margin 打回污染值(`-160`),形成死循环(实测:set contentSize=1600 → 实际返回 1920 且 margin 弹回 -160)。正确路径实测:先恢复 `_enabled=true`,再设 `left=0`、`right=0`,`contentSize` 自动从 1920 收敛到 1600。已落地样板:`should_hide_in_hierarchy`(`left/right=0 + contentSize=1600 + originalWidth=0`),而 `Login_Layer`/`group-2` 污染时 `originalWidth=100`。
---
## 6. 非目标(YAGNI)
- **不做嵌套节点树的进一步细分**。转换只产出「Layer → Group → 对象」两层结构;更细的节点树由用户在编辑器中人工组织。
- **不识别按钮/交互组件**。事件信息保留在 `legacy`,由 C 处理。
- **不迁移任何平台逻辑**。本 spec 只做静态 UI;逻辑属 C。
- **不处理图片本身**(背景拉伸只设 Widget,不做缩放/重采样;美术后续手动更换)。
- **不重命名资源为语义名**(§3.6)。
- **不做逐帧动画的时间轴**。`flags.animation` 只做标记。
---
## 7. 与其它 spec 的接口
- **`2026-08-27-ui-asset-and-skin-design.md`**:切出的散图落入其定义的 8 个可覆盖目录;bucket 划分沿用其 §2 规则 3 的目录表;label 样式取自其 §4 的 `theme.fonts`。
- **子系统 B(引擎机制映射)→ `2026-08-30-legacy-engine-api-mapping.md`**:需要 A 产出的 `legacy.objectId` / `groupId` / `events`;其产出的属性号对照表是 C 的前置。
- **子系统 C(平台逻辑移植)→ `2026-08-30-legacy-platform-logic-migration.md`**:依赖 A 的 ObjectID → 节点路径全局索引(§1.3)与 B 的 API 映射表;并定义四层架构映射、状态归属边界、核心流程一致性检查点。
- **组件替换 spec(待写)**:`SeatView` / 按人数布局。牌桌内的 `Player_Head_Score_Layer`(60 个对象)由 A 转出静态版本后,该 spec 决定哪些改为按人数动态生成。