fix(scripts): final review Critical+Important fixes (Widget alignment, full readLayerXml, Label _string, atlas selection, hard-coded paths, exit code, dead code removal)

This commit is contained in:
2026-09-02 09:11:51 +08:00
parent 7e0a8b79a4
commit cb3bd54175
19 changed files with 5076 additions and 186 deletions
@@ -1,11 +0,0 @@
import { readFileSync } from 'node:fs';
import iconv from 'iconv-lite';
const buf = readFileSync('framework-tests/legacy-layer-migration/fixtures/Layer00002.xml');
// NOTE: 实际 Layer00002.xml 是 UTF-8 编码 (XML 头声明 + 字节验证),
// spec/brief 要求的 GB18030 在该文件上会乱码 (微信登录背景 不可读).
// 转换工具应支持两种编码, 优先按 XML 声明, 否则按 GB18030 fallback.
const text = iconv.decode(buf, 'utf8');
console.log('decoded length:', text.length);
console.log('contains 微信登录背景:', text.includes('微信登录背景'));
console.log('contains Login_Layer:', text.includes('Login_Layer'));
@@ -1,6 +1,6 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { computeLpos, computeContentSize } from '../../scripts/legacy-layer-to-cocos-prefab.mjs';
import { computeLpos, computeContentSize, computeWidgetAlignment } from '../../scripts/legacy-layer-to-cocos-prefab.mjs';
test('computeLpos 锚点转换(实测验证:渠道logo)', () => {
const spirit = { x: '734', y: '143', w: '59', h: '148' };
@@ -47,4 +47,49 @@ test('computeContentSize 默认 SizeCalcMode=0(字段缺失视作 0)', () =>
test('computeContentSize SizeCalcMode=1 → throw(不静默)', () => {
const spirit = { w: '100', h: '50', sizeCalcMode: '1' };
assert.throws(() => computeContentSize(spirit), /SizeCalcMode=1.*reverse gameabc\.min\.js/);
});
test('computeContentSize SizeCalcMode=2 uses ImgResID natural size and relative ratios', () => {
const spirit = {
id: '2', imgResID: '20', w: '1280', h: '720', sizeCalcMode: '2',
sizeRelaWidth: '10000', sizeRelaHeight: '10000',
};
const size = computeContentSize(spirit);
assert.equal(size.width, 1280);
assert.equal(size.height, 720);
});
test('computeWidgetAlignment derives full-screen, normal, and top-left alignment cases', () => {
const fullScreen = computeWidgetAlignment({
name: '微信登录背景', x: '0', y: '0', w: '1280', h: '720', lpos: { x: 0, y: 0 },
});
assert.equal(fullScreen._alignFlags, 45);
assert.equal(fullScreen._left, 0);
assert.equal(fullScreen._right, 0);
assert.equal(fullScreen._top, 0);
assert.equal(fullScreen._bottom, 0);
assert.equal(fullScreen._horizontalCenter, undefined);
assert.equal(fullScreen._verticalCenter, undefined);
assert.equal(fullScreen._originalWidth, 100);
assert.equal(fullScreen._originalHeight, 100);
const normal = computeWidgetAlignment({
name: '渠道logo', x: '734', y: '143', w: '59', h: '148', lpos: { x: 123.5, y: 0 },
});
assert.equal(normal._alignFlags, 16);
assert.equal(normal._horizontalCenter, 123.5);
assert.equal(normal._verticalCenter, 0);
assert.equal(normal._left, undefined);
assert.equal(normal._right, undefined);
assert.equal(normal._top, undefined);
assert.equal(normal._bottom, undefined);
const topLeft = computeWidgetAlignment({
name: '版本显示', x: '16', y: '9', w: '96', h: '31',
});
assert.equal(topLeft._alignFlags, 9);
assert.equal(topLeft._left, -576);
assert.equal(topLeft._top, 335.5);
assert.equal(topLeft._horizontalCenter, undefined);
assert.equal(topLeft._verticalCenter, undefined);
});
@@ -34,7 +34,7 @@
"rationale": "直接读精灵自身 Width/Height 字段作为 _contentSize; gameabc.min.js 中 SizeCalcMode=0 是最简路径 (背景图 Layer00001/00002 全屏 1280×720 即此路径)"
},
"2": {
"formula": "parent_size = lookup_Spirit(Spirit.SizeParentID)._contentSize; _contentSize = { width: parent_size.width * Spirit.SizeRelaWidth / 10000, height: parent_size.height * Spirit.SizeRelaHeight / 10000 }; 当 WidthCalcMode=0 时回退到 Spirit.Width (parseFloat), 当 WidthCalcMode=1 时用 SizeRelaWidth 公式; HeightCalcMode 同理",
"formula": "image_natural_size = lookup gameabc_Image.json ImageFileList[imgResID === Spirit.ImgResID].w_all/h_all; parent_size = { width: image_natural_size.width, height: image_natural_size.height }; _contentSize = { width: parent_size.width * Spirit.SizeRelaWidth / 10000, height: parent_size.height * Spirit.SizeRelaHeight / 10000 }; 当 WidthCalcMode=0 时回退到 Spirit.Width (parseFloat), 当 WidthCalcMode=1 时用 SizeRelaWidth 公式; HeightCalcMode 同理",
"rationale": "Layer00001 '友乐背景' (SizeCalcMode=2, SizeParentID=21, SizeRelaWidth=10000) / Layer00002 '微信登录背景' (SizeCalcMode=2, SizeParentID=20, SizeRelaWidth=10000) 全量采样: 全部 WidthCalcMode=1, HeightCalcMode=1, SizeRelaWidth=10000, SizeRelaHeight=10000; 即按 100% 比例继承父 spirit 尺寸; 这种 pattern 用于 '充满父容器' 的背景 (1280×720 全屏背景), SizeParentID 指向另一个 1280×720 的 spirit"
}
},
@@ -1,7 +1,9 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { readLayerXml, emitPrefabJson } from '../../scripts/legacy-layer-to-cocos-prefab.mjs';
import { ROOT, HOST_PROJECT } from '../../scripts/lib/paths.mjs';
// PoC 阶段 3 端到端: Layer00002.xml Login_Layer (18 spirits, group 2, 包含 Spirit405 渠道logo +
// Spirit177 游客登录 anchor-test cases). 验证生成 prefab 与 fixtures/Login_Layer.prefab 在
@@ -16,8 +18,8 @@ import { readLayerXml, emitPrefabJson } from '../../scripts/legacy-layer-to-coco
// layer.spirits 只保留 id=405 (渠道logo) + id=177 (游客登录) 两个 anchor-test case, 完全避开
// ImgResID 427 的依赖. 这是 Task 11 fix round 1 的 spec intent: 验证 anchor 公式精度,
// 而不是验证 CLI 完整管线 (后者已由 poc-iconbutton.test.mjs / poc-protocol.test.mjs 覆盖).
const FIXTURE_XML = 'G:/Works/YouleGamesCocosCreator/cocoscreator_projects/framework-tests/legacy-layer-migration/fixtures/Layer00002.xml';
const REFERENCE_PREFAB = 'G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/Login_Layer.prefab';
const FIXTURE_XML = join(ROOT, 'framework-tests', 'legacy-layer-migration', 'fixtures', 'Layer00002.xml');
const REFERENCE_PREFAB = join(HOST_PROJECT, 'assets', 'framework', 'ui', 'prefabs', 'Login_Layer.prefab');
function buildFilteredLayer() {
const layer = readLayerXml(FIXTURE_XML);
@@ -27,6 +29,43 @@ function buildFilteredLayer() {
return layer;
}
function nodeComponent(json, node, componentType) {
return json[node._components.map(ref => ref.__id__).find(index => json[index]?.__type__ === componentType)];
}
test('Login_Layer full-screen and normal node geometry matches reference strictly', () => {
const layer = readLayerXml(FIXTURE_XML);
layer.spirits = layer.spirits.filter(s => s.id === '2' || s.id === '405');
const generated = emitPrefabJson(layer);
const expected = JSON.parse(readFileSync(REFERENCE_PREFAB, 'utf8'));
const background = generated.find(o => o.__type__ === 'cc.Node' && o._name === '微信登录背景');
const backgroundExpected = expected.find(o => o.__type__ === 'cc.Node' && o._name === '微信登录背景');
assert.ok(background);
assert.ok(backgroundExpected);
assert.equal(background._lpos.x, backgroundExpected._lpos.x);
assert.equal(background._lpos.y, backgroundExpected._lpos.y);
assert.equal(nodeComponent(generated, background, 'cc.Widget')._alignFlags, 45);
assert.equal(nodeComponent(generated, background, 'cc.Widget')._originalWidth, 100);
assert.equal(nodeComponent(generated, background, 'cc.Widget')._originalHeight, 100);
assert.equal(nodeComponent(generated, background, 'cc.UITransform')._contentSize.width, 1600);
assert.equal(nodeComponent(generated, background, 'cc.UITransform')._contentSize.height, 720);
const normal = generated.find(o => o.__type__ === 'cc.Node' && o._name === 'Spirit405');
const normalExpected = expected.find(o => o.__type__ === 'cc.Node' && o._name === '渠道logo');
assert.ok(normal);
assert.ok(normalExpected);
assert.equal(normal._lpos.x, normalExpected._lpos.x);
assert.equal(normal._lpos.y, normalExpected._lpos.y);
assert.equal(nodeComponent(generated, normal, 'cc.Widget')._alignFlags, 16);
assert.equal(nodeComponent(generated, normal, 'cc.Widget')._horizontalCenter, 123.5);
assert.equal(nodeComponent(generated, normal, 'cc.Widget')._verticalCenter, 0);
assert.equal(nodeComponent(generated, normal, 'cc.Widget')._left, undefined);
assert.equal(nodeComponent(generated, normal, 'cc.Widget')._top, undefined);
assert.equal(nodeComponent(generated, normal, 'cc.UITransform')._contentSize.width, 59);
assert.equal(nodeComponent(generated, normal, 'cc.UITransform')._contentSize.height, 148);
});
test('PoC 阶段 3 - 渠道logo _lpos 误差 < 0.001 (bypass CLI to avoid sprite atlas dep)', () => {
const layer = buildFilteredLayer();
const json = emitPrefabJson(layer);
@@ -10,7 +10,7 @@ import { existsSync, readFileSync } from 'node:fs';
// Layer00010.xml 4 个 spirit 全属 group 37 (无 Layer 根直挂), 生成的 prefab 应有:
// - 1 root Widget (default _alignFlags=45, 全屏铺满)
// - 1 group-37 Widget (default _alignFlags=45)
// - 4 Spirit Widget (apply computeWidgetAlignment: _alignFlags=24=16|8 水平+垂直居中)
// - 4 Spirit Widget (apply computeWidgetAlignment: normal spirits use _alignFlags=16)
const CLI = 'node scripts/legacy-layer-to-cocos-prefab-cli.mjs';
const INPUT = '../projects/Game_Surface_3/save/Layer00010.xml';
const TARGET = 'YouleNexus/assets/framework/ui/prefabs/widgets/Layer10_Protocol.prefab';
@@ -32,7 +32,7 @@ test('Layer10_Protocol 生成 + 含 cc.Widget 节点 (computeWidgetAlignment 锚
`应有 ≥6 个 cc.Widget 节点 (1 root + 1 group + 4 spirit), 实得 ${widgetNodes.length}`
);
// 2) Spirit Widget 应 _alignFlags=24 (16|8 水平+垂直居中) — 由 computeWidgetAlignment 算出.
// 2) Spirit Widget 应使用 normal spirit 的 _alignFlags=16 — 由 computeWidgetAlignment 算出.
// 这是 PoC 阶段 2 的关键验证: 证明 computeWidgetAlignment 真实生效 (而非全部用默认 45).
// Spirit Widget 与 root/group Widget 的区别: Widget.node.__id__ 指向的 cc.Node
// _name 不为 'Protol_Layer' (root) 也不以 'group-' 开头 (group 父节点).
@@ -48,7 +48,7 @@ test('Layer10_Protocol 生成 + 含 cc.Widget 节点 (computeWidgetAlignment 锚
`应有 4 个 Spirit Widget, 实得 ${spiritWidgetCount}`
);
// 3) Spirit Widget _alignFlags 必须等于 24 (= 16 | 8, 水平居中 + 垂直居中).
// 3) Spirit Widget _alignFlags 必须等于 16 (normal spirit = horizontal center).
// 这条断言专门验证 computeWidgetAlignment 已被实际调用 — 未实现时所有 Widget
// 都用默认 _alignFlags=45, 此断言必 FAIL.
const spiritWidgets = widgetNodes.filter(w => {
@@ -60,9 +60,11 @@ test('Layer10_Protocol 生成 + 含 cc.Widget 节点 (computeWidgetAlignment 锚
&& !targetNode._name.startsWith('group-');
});
for (const w of spiritWidgets) {
const targetNode = json[w.node.__id__];
const expectedFlags = targetNode._name === '协议遮罩' ? 45 : targetNode._name === '协议背景' ? 9 : 16;
assert.equal(
w._alignFlags, 24,
`Spirit Widget 应 _alignFlags=24 (16|8), 实得 ${w._alignFlags}`
w._alignFlags, expectedFlags,
`Spirit Widget ${targetNode._name} 应 _alignFlags=${expectedFlags}, 实得 ${w._alignFlags}`
);
}
});
@@ -27,6 +27,30 @@ test('emitPrefabJson 含 PrefabInfo 块', () => {
assert.equal(prefabInfo.asset.__id__, 0);
});
test('emitPrefabJson uses XML Text for labels and targetName overrides the output name', () => {
const layer = {
layerId: '10', layerName: 'Protol_Layer', spirits: [{
id: '1', name: 'Spirit1', text: '登录', type: '1', fontSize: '18', fontColor: '16777215',
alignHorz: '0', alignVert: '1', fontBold: '0', wordWrap: '0', lineSpace: '0', rowSpace: '0',
x: '100', y: '100', w: '100', h: '30', group: '37', indexOfLayer: '1',
}],
};
const json = emitPrefabJson(layer, { targetName: 'Layer10_Protocol' });
assert.equal(json[0]._name, 'Layer10_Protocol');
const textNode = json.find(o => o.__type__ === 'cc.Node' && o._name === 'Spirit1');
assert.ok(textNode);
const label = json.filter(o => o.__type__ === 'cc.Label')[0];
assert.equal(label._string, '登录');
assert.equal(label._fontSize, 18);
assert.equal(label._color.r, 255);
assert.equal(label._color.g, 255);
assert.equal(label._color.b, 255);
assert.equal(label._horizontalAlign, 0);
assert.equal(label._verticalAlign, 1);
assert.equal(label._isBold, false);
assert.equal(label._enableWrapText, false);
});
test('emitPrefabJson 渠道logo _lpos 误差 < 0.001', () => {
// 注: brief 原版用 spirit name='Spirit405' 但断言搜 /渠道/; 实际 fixture 里 id=405 的 spirit
// 坐标 (734, 143, 59, 148) 严格对应 Login_Layer '渠道logo' 节点; 这里 name 用 '渠道logo'
@@ -1,12 +1,12 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { mapSpiritTypeToComponent } from '../../scripts/legacy-layer-to-cocos-prefab.mjs';
import { ROOT } from '../../scripts/lib/paths.mjs';
// 绝对路径 -- tests 从 cocoscreator_projects/ 运行, 相对路径会解析到仓库根,
// 漏掉 cocoscreator_projects/ 段导致 ENOENT (Task 2 fix round 1 Finding 3).
const SEMANTICS = JSON.parse(readFileSync(
'G:/Works/YouleGamesCocosCreator/cocoscreator_projects/framework-tests/legacy-layer-migration/fixtures/spirit-semantics.json',
join(ROOT, 'framework-tests', 'legacy-layer-migration', 'fixtures', 'spirit-semantics.json'),
'utf8'
));
@@ -1,9 +1,11 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { ROOT } from '../../scripts/lib/paths.mjs';
const SEMANTICS = JSON.parse(readFileSync(
'G:/Works/YouleGamesCocosCreator/cocoscreator_projects/framework-tests/legacy-layer-migration/fixtures/spirit-semantics.json',
join(ROOT, 'framework-tests', 'legacy-layer-migration', 'fixtures', 'spirit-semantics.json'),
'utf8'
));
@@ -1,6 +1,6 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { resolveSpriteFrameUuid } from '../../scripts/legacy-layer-to-cocos-prefab.mjs';
import { resolveSpriteFrameUuid, selectAtlasForLayerName } from '../../scripts/legacy-layer-to-cocos-prefab.mjs';
// 数据源权威: gameabc_Image.json id=20 -> bmp="00020.png" 位于 atlas-login/,
// 该 .meta 的 subMetas.f9941.uuid 为 e36acab8-e623-4bfb-af05-3c6fd2fddfb4@f9941
@@ -12,6 +12,12 @@ test('resolveSpriteFrameUuid("20") 返回 atlas-login/00020.png 的 SpriteFrame
assert.match(uuid, /@f9941$/, '必须含 SpriteFrame sub-asset 后缀 @f9941');
});
test('layer name prefix selects the expected atlas directory', () => {
assert.equal(selectAtlasForLayerName('Login_Layer'), 'atlas-login');
assert.equal(selectAtlasForLayerName('MainScene_PlayerInfo'), 'atlas-hall');
assert.equal(selectAtlasForLayerName('Other_Layer'), 'atlas-login');
});
test('未知 imgResId → throw(不静默兜底)', () => {
// 数据源权威唯一、下游不兜底: ImgResID 不在 gameabc_Image.json 时必须显式报错.
assert.throws(
@@ -1,10 +1,10 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { join } from 'node:path';
import { readLayerXml } from '../../scripts/legacy-layer-to-cocos-prefab.mjs';
import { ROOT } from '../../scripts/lib/paths.mjs';
// 绝对路径 -- tests 从 cocoscreator_projects/ 运行, 相对路径会解析到仓库根,
// 漏掉 cocoscreator_projects/ 段导致 ENOENT (Task 2 fix round 1 Finding 3).
const FIXTURE = 'G:/Works/YouleGamesCocosCreator/cocoscreator_projects/framework-tests/legacy-layer-migration/fixtures/Layer00002.xml';
const FIXTURE = join(ROOT, 'framework-tests', 'legacy-layer-migration', 'fixtures', 'Layer00002.xml');
test('readLayerXml 解码 UTF-8 并提取 Layer00002 字段', () => {
const layer = readLayerXml(FIXTURE);
@@ -41,3 +41,34 @@ test('BelongGroupID 提取(group=0 节点也存在)', () => {
const g0 = layer.spirits.filter(s => s.group === '0');
assert.ok(g0.length >= 0); // Login_Layer 没有 group=0,但提取逻辑应支持
});
test('readLayerXml 保留 SizeCalcMode、坐标、归属与 type-1 字体契约字段', () => {
const layer = readLayerXml(FIXTURE);
const background = layer.spirits.find(s => s.id === '2');
const text = layer.spirits.find(s => s.id === '285');
assert.ok(background);
assert.ok(text);
assert.equal(background.text, undefined);
assert.equal(background.sizeCalcMode, '2');
assert.equal(background.sizeParentID, '20');
assert.equal(background.widthCalcMode, '1');
assert.equal(background.heightCalcMode, '1');
assert.equal(background.sizeRelaWidth, '10000');
assert.equal(background.sizeRelaHeight, '10000');
assert.equal(background.coordSelfPos, '0');
assert.equal(background.coordRelaX, '0');
assert.equal(background.coordRelaY, '0');
assert.equal(background.coordParentID, '0');
assert.equal(background.belongLayerID, '2');
assert.equal(text.text, '版本显示');
assert.equal(text.fontSize, '18');
assert.equal(text.fontColor, '16777215');
assert.equal(text.alignHorz, '0');
assert.equal(text.alignVert, '0');
assert.equal(text.fontBold, '0');
assert.equal(text.wordWrap, '0');
assert.equal(text.lineSpace, '0');
assert.equal(text.rowSpace, undefined);
});