diff --git a/cocoscreator_projects/framework-tests/legacy-layer-migration/prefab-emitter.test.mjs b/cocoscreator_projects/framework-tests/legacy-layer-migration/prefab-emitter.test.mjs new file mode 100644 index 0000000..0146810 --- /dev/null +++ b/cocoscreator_projects/framework-tests/legacy-layer-migration/prefab-emitter.test.mjs @@ -0,0 +1,51 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { emitPrefabJson } from '../../scripts/legacy-layer-to-cocos-prefab.mjs'; + +test('emitPrefabJson 输出是 JSON 数组', () => { + const layer = { layerId: '2', layerName: 'Login_Layer', spirits: [] }; + const json = emitPrefabJson(layer); + assert.ok(Array.isArray(json), '输出必须是数组'); + assert.ok(json.length >= 1, '至少含 cc.Prefab 头'); + assert.equal(json[0].__type__, 'cc.Prefab'); + assert.equal(json[0]._name, 'Login_Layer'); +}); + +test('emitPrefabJson 含 CompPrefabInfo 引用块', () => { + const layer = { layerId: '2', layerName: 'Login_Layer', spirits: [] }; + const json = emitPrefabJson(layer); + const compInfo = json.filter(o => o.__type__ === 'cc.CompPrefabInfo'); + assert.ok(compInfo.length > 0, '每个组件应有 CompPrefabInfo 引用块'); +}); + +test('emitPrefabJson 含 PrefabInfo 块', () => { + const layer = { layerId: '2', layerName: 'Login_Layer', spirits: [] }; + const json = emitPrefabJson(layer); + const prefabInfo = json.find(o => o.__type__ === 'cc.PrefabInfo'); + assert.ok(prefabInfo, '必须有 PrefabInfo 块'); + assert.equal(prefabInfo.root.__id__, 1); // 根节点 id=1 + assert.equal(prefabInfo.asset.__id__, 0); +}); + +test('emitPrefabJson 渠道logo _lpos 误差 < 0.001', () => { + // 注: brief 原版用 spirit name='Spirit405' 但断言搜 /渠道/; 实际 fixture 里 id=405 的 spirit + // 坐标 (734, 143, 59, 148) 严格对应 Login_Layer '渠道logo' 节点; 这里 name 用 '渠道logo' + // 与断言一致, 否则 node 找不到。 + const layer = { + layerId: '2', layerName: 'Login_Layer', + spirits: [ + { id: '2', name: '微信登录背景', type: '0', imgResID: '20', + x: '0', y: '0', w: '1280', h: '720', group: '2', indexOfLayer: '1' }, + { id: '405', name: '渠道logo', type: '0', imgResID: '5', + x: '734', y: '143', w: '59', h: '148', group: '2', indexOfLayer: '2' }, + { id: '285', name: '版本显示', type: '1', imgResID: '', + x: '16', y: '9', w: '96', h: '31', group: '2', indexOfLayer: '3' }, + ], + }; + const json = emitPrefabJson(layer); + // Find 渠道logo node + const genLogo = json.find(o => o.__type__ === 'cc.Node' && o._name && o._name.includes('渠道')); + assert.ok(genLogo, '应有渠道logo node'); + assert.ok(Math.abs(genLogo._lpos.x - 123.5) < 0.001, `x expected 123.5 got ${genLogo._lpos.x}`); + assert.ok(Math.abs(genLogo._lpos.y - 143) < 0.001, `y expected 143 got ${genLogo._lpos.y}`); +}); \ No newline at end of file diff --git a/cocoscreator_projects/scripts/legacy-layer-to-cocos-prefab.mjs b/cocoscreator_projects/scripts/legacy-layer-to-cocos-prefab.mjs index 2a39acb..18f07a5 100644 --- a/cocoscreator_projects/scripts/legacy-layer-to-cocos-prefab.mjs +++ b/cocoscreator_projects/scripts/legacy-layer-to-cocos-prefab.mjs @@ -257,3 +257,447 @@ export function resolveSpriteFrameUuid(imgResId) { } return spriteFrame.uuid; } + +// ============================================================================ +// emitPrefabJson — Layer → Cocos prefab JSON 数组 (Task 8) +// +// Schema 严格按 fixtures/Login_Layer.prefab (已实测): cc.Prefab 头 + cc.Node 根 + +// group-X 父节点 + 每个 Spirit (Node + UITransform + CompPrefabInfo + 主组件 + +// CompPrefabInfo + Widget + CompPrefabInfo + PrefabInfo) + 末尾 group/root 的 +// UITransform/Widget/PrefabInfo 块. +// +// ID 分配: cc.Prefab 在 array[0] (无 __id__ 字段, 仅 data.__id__=1 引用根); +// 从 array[1] 开始 __id__ = array-index. root=1, group-N=2..K, 然后 spirits +// 按 group-then-IndexOfLayer 顺序, 每个 spirit 占 8 个 id (1 Node + 1 UIT + +// 1 CompInfo + 1 主组件 + 1 CompInfo + 1 Widget + 1 CompInfo + 1 PrefabInfo). +// group-N 的 UIT/Widget/PrefabInfo 排在所有 spirits 之后, root 的排在最后. +// +// 设计分辨率: 1600×720 (YouleNexus project.json), 与 Login_Layer.prefab 中 +// group/root UITransform._contentSize 一致. +// ============================================================================ + +const DESIGN_WIDTH = 1600; +const DESIGN_HEIGHT = 720; + +function randomFileId() { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let id = ''; + for (let i = 0; i < 22; i++) { + id += chars[Math.floor(Math.random() * chars.length)]; + } + return id; +} + +function makeNode(name, parentRef) { + return { + __type__: 'cc.Node', + _name: name, + _objFlags: 0, + __editorExtras__: {}, + _parent: parentRef, + _children: [], + _active: true, + _components: [], + _prefab: null, + _lpos: { __type__: 'cc.Vec3', x: 0, y: 0, z: 0 }, + _lrot: { __type__: 'cc.Quat', x: 0, y: 0, z: 0, w: 1 }, + _lscale: { __type__: 'cc.Vec3', x: 1, y: 1, z: 1 }, + _mobility: 0, + _layer: 1073741824, + _euler: { __type__: 'cc.Vec3', x: 0, y: 0, z: 0 }, + _id: '', + }; +} + +function makeCompPrefabInfo() { + return { + __type__: 'cc.CompPrefabInfo', + fileId: randomFileId(), + }; +} + +function makePrefabInfo() { + return { + __type__: 'cc.PrefabInfo', + root: { __id__: 1 }, + asset: { __id__: 0 }, + fileId: randomFileId(), + instance: null, + targetOverrides: null, + nestedPrefabInstanceRoots: null, + }; +} + +function makeUiTransform(nodeId, compPrefabId, width, height) { + return { + __type__: 'cc.UITransform', + _name: '', + _objFlags: 0, + __editorExtras__: {}, + node: { __id__: nodeId }, + _enabled: true, + __prefab: { __id__: compPrefabId }, + _contentSize: { __type__: 'cc.Size', width, height }, + _anchorPoint: { __type__: 'cc.Vec2', x: 0.5, y: 0.5 }, + _id: '', + }; +} + +function makeWidget(nodeId, compPrefabId) { + return { + __type__: 'cc.Widget', + _name: '', + _objFlags: 0, + __editorExtras__: {}, + node: { __id__: nodeId }, + _enabled: true, + __prefab: { __id__: compPrefabId }, + _alignFlags: 45, + _target: null, + _left: 0, _right: 0, _top: 0, _bottom: 0, + _horizontalCenter: 0, _verticalCenter: 0, + _isAbsLeft: true, _isAbsRight: true, _isAbsTop: true, _isAbsBottom: true, + _isAbsHorizontalCenter: true, _isAbsVerticalCenter: true, + _originalWidth: 0, _originalHeight: 0, + _alignMode: 2, + _lockFlags: 0, + _id: '', + }; +} + +function makeSprite(nodeId, compPrefabId, spriteFrameUuid, type = 0) { + return { + __type__: 'cc.Sprite', + _name: '', + _objFlags: 0, + __editorExtras__: {}, + node: { __id__: nodeId }, + _enabled: true, + __prefab: { __id__: compPrefabId }, + _customMaterial: null, + _srcBlendFactor: 2, + _dstBlendFactor: 4, + _color: { __type__: 'cc.Color', r: 255, g: 255, b: 255, a: 255 }, + _spriteFrame: spriteFrameUuid + ? { __uuid__: spriteFrameUuid, __expectedType__: 'cc.SpriteFrame' } + : null, + _type: type, // 0=SIMPLE, 1=SLICED, 2=TILED, 3=FILLED + _fillType: 0, + _sizeMode: 0, + _fillCenter: { __type__: 'cc.Vec2', x: 0, y: 0 }, + _fillStart: 0, + _fillRange: 0, + _isTrimmedMode: true, + _useGrayscale: false, + _atlas: null, + _id: '', + }; +} + +function makeLabel(nodeId, compPrefabId, text) { + return { + __type__: 'cc.Label', + _name: '', + _objFlags: 0, + __editorExtras__: {}, + node: { __id__: nodeId }, + _enabled: true, + __prefab: { __id__: compPrefabId }, + _customMaterial: null, + _srcBlendFactor: 2, + _dstBlendFactor: 4, + _color: { __type__: 'cc.Color', r: 255, g: 255, b: 255, a: 255 }, + _string: text || '', + _horizontalAlign: 1, + _verticalAlign: 1, + _actualFontSize: 40, + _fontSize: 40, + _fontFamily: 'Arial', + _lineHeight: 40, + _overflow: 0, + _enableWrapText: true, + _font: null, + _isSystemFontUsed: true, + _spacingX: 0, + _isItalic: false, _isBold: false, _isUnderline: false, + _underlineHeight: 2, + _cacheMode: 0, + _enableOutline: false, + _outlineColor: { __type__: 'cc.Color', r: 0, g: 0, b: 0, a: 255 }, + _outlineWidth: 2, + _enableShadow: false, + _shadowColor: { __type__: 'cc.Color', r: 0, g: 0, b: 0, a: 255 }, + _shadowOffset: { __type__: 'cc.Vec2', x: 2, y: 2 }, + _shadowBlur: 2, + _id: '', + }; +} + +function makeProgressBar(nodeId, compPrefabId, spriteFrameUuid) { + // cc.ProgressBar 简化 schema — 真实 fixtures 中 Layer 含 type=3 spirit 但 + // Login_Layer.prefab fixture 无 ProgressBar 节点. 字段尽量按 Cocos 3.8 实际 schema 写, + // 缺资源时降级为 null (非 unknown; 数据源权威缺失由调用方上游补). + return { + __type__: 'cc.ProgressBar', + _name: '', + _objFlags: 0, + __editorExtras__: {}, + node: { __id__: nodeId }, + _enabled: true, + __prefab: { __id__: compPrefabId }, + _customMaterial: null, + _srcBlendFactor: 2, + _dstBlendFactor: 4, + _color: { __type__: 'cc.Color', r: 255, g: 255, b: 255, a: 255 }, + _spriteFrame: spriteFrameUuid + ? { __uuid__: spriteFrameUuid, __expectedType__: 'cc.SpriteFrame' } + : null, + _type: 0, + _reverse: false, + _totalLength: 100, + _progress: 0, + _barSprite: null, + _mode: 0, + _id: '', + }; +} + +function makeEditBox(nodeId, compPrefabId, spriteFrameUuid, text) { + // cc.EditBox 简化 schema — 同上, fixture 无 EditBox 节点, 字段按 Cocos 3.8 实际 schema. + return { + __type__: 'cc.EditBox', + _name: '', + _objFlags: 0, + __editorExtras__: {}, + node: { __id__: nodeId }, + _enabled: true, + __prefab: { __id__: compPrefabId }, + _customMaterial: null, + _srcBlendFactor: 2, + _dstBlendFactor: 4, + _color: { __type__: 'cc.Color', r: 255, g: 255, b: 255, a: 255 }, + _string: text || '', + _placeholder: '', + _placeholderColor: { __type__: 'cc.Color', r: 127, g: 127, b: 127, a: 255 }, + _maxLength: 20, + _inputMode: 0, + _inputFlag: 0, + _returnType: 0, + _backgroundImage: spriteFrameUuid + ? { __uuid__: spriteFrameUuid, __expectedType__: 'cc.SpriteFrame' } + : null, + _fontColor: { __type__: 'cc.Color', r: 255, g: 255, b: 255, a: 255 }, + _fontSize: 20, + _lineHeight: 40, + _fontFamily: 'Arial', + _isSystemFontUsed: true, + _tabIndex: 0, + _editingDidBegin: null, + _textChanged: null, + _editingDidEnded: null, + _editingReturn: null, + _stayOnTop: false, + _id: '', + }; +} + +/** + * 将 parsed Layer 对象转为 Cocos prefab JSON 数组, schema 严格按 + * fixtures/Login_Layer.prefab. + * + * 字段需求 (数据源权威唯一): + * layer.layerName — 必有; cc.Prefab._name / 根节点._name + * layer.spirits[*].id — 必有; spirit 唯一标识 + * layer.spirits[*].name — 必有; 节点._name + * layer.spirits[*].type — 必有; 走 mapSpiritTypeToComponent (未知 throw) + * layer.spirits[*].x/y/w/h — 必有; 走 computeLpos + computeContentSize + * layer.spirits[*].group/indexOfLayer — 必有; 走 buildGroupParents + * layer.spirits[*].imgResID — Sprite (type=0/5) 必有; 走 resolveSpriteFrameUuid + * + * ID 分配策略 (与 Login_Layer.prefab 1:1 对齐): + * - cc.Prefab 在 array[0], 无 __id__ (data.__id__=1 引用根) + * - 数组下标 i ≥ 1 的对象 __id__ = i + * - 根节点 = array[1] (id=1) + * - group-X 父节点顺序: 按 group ID 升序排, 紧随根节点 (id=2..K) + * - Spirit 顺序: 按 group 升序, group 内按 IndexOfLayer 升序 + * 每个 spirit 占 8 个 id: Node UIT CompInfo 主组件 CompInfo Widget CompInfo PrefabInfo + * - group 末尾追加: group-X 的 UIT CompInfo Widget CompInfo PrefabInfo (5 id) + * - root 末尾追加: root 的 UIT CompInfo Widget CompInfo PrefabInfo (5 id) + * + * @param {{ layerId?: string, layerName: string, spirits: Array<{ id: string, name: string, type: string, imgResID: string, x: string, y: string, w: string, h: string, group: string, indexOfLayer: string }> }} layer + * @returns {Array} Cocos prefab JSON 数组 + */ +export function emitPrefabJson(layer) { + if (!layer || !layer.layerName) { + throw new Error('emitPrefabJson: layer.layerName is required'); + } + if (!Array.isArray(layer.spirits)) { + throw new Error('emitPrefabJson: layer.spirits must be an array'); + } + + const out = []; + + // ── 1. cc.Prefab 头 ───────────────────────────────────────────── + out.push({ + __type__: 'cc.Prefab', + _name: layer.layerName, + _objFlags: 0, + __editorExtras__: {}, + _native: '', + data: { __id__: 1 }, // 根节点 id + optimizationPolicy: 0, + persistent: false, + }); + + // ── 2. 根节点 cc.Node (id=1) ──────────────────────────────────── + const rootNode = makeNode(layer.layerName, null); + out.push(rootNode); + const rootId = out.length - 1; // =1 + + // ── 3. group-X 父节点 ────────────────────────────────────────── + const { groupParents, rootChildSpiritIds } = buildGroupParents(layer.spirits); + const spiritById = new Map(layer.spirits.map(s => [s.id, s])); + + const groupInfo = []; // [{ group, node, nodeId }] + for (const g of groupParents) { + const node = makeNode(g.name, { __id__: rootId }); + out.push(node); + groupInfo.push({ group: g, node, nodeId: out.length - 1 }); + } + + // ── 4. Spirit 子节点 + 组件 ──────────────────────────────────── + const spiritInfo = new Map(); // spiritId → { node, nodeId, prefabInfoId } + + for (const gi of groupInfo) { + for (const spiritId of gi.group.childSpiritIds) { + const spirit = spiritById.get(spiritId); + if (!spirit) { + throw new Error(`emitPrefabJson: spirit id ${spiritId} referenced by ${gi.group.name} but not found in layer.spirits`); + } + const info = buildSpiritBlock(spirit, { __id__: gi.nodeId }, out); + spiritInfo.set(spiritId, info); + gi.node._children.push({ __id__: info.nodeId }); + } + } + + for (const spiritId of rootChildSpiritIds) { + const spirit = spiritById.get(spiritId); + if (!spirit) { + throw new Error(`emitPrefabJson: root child spirit id ${spiritId} not found in layer.spirits`); + } + const info = buildSpiritBlock(spirit, { __id__: rootId }, out); + spiritInfo.set(spiritId, info); + rootNode._children.push({ __id__: info.nodeId }); + } + + // ── 5. group-X 末尾 UITransform + Widget + PrefabInfo ────────── + for (const gi of groupInfo) { + appendNodeEnvelope(gi.node, gi.nodeId, out); + } + + // ── 6. 根节点末尾 UITransform + Widget + PrefabInfo ─────────── + appendNodeEnvelope(rootNode, rootId, out); + + return out; +} + +/** + * 构造单个 Spirit 的所有对象 (Node + UITransform + CompPrefabInfo + 主组件 + + * CompPrefabInfo + Widget + CompPrefabInfo + PrefabInfo), 按 schema 顺序 push + * 到 out 数组, 返回关键 id 供外层 wiring. + */ +function buildSpiritBlock(spirit, parentRef, out) { + const lpos = computeLpos(spirit); + const size = computeContentSize(spirit); + const componentName = mapSpiritTypeToComponent(spirit.type); + + // 1. cc.Node (主组件 id=K+0) + const node = makeNode(spirit.name, parentRef); + node._lpos = { __type__: 'cc.Vec3', x: lpos.x, y: lpos.y, z: lpos.z }; + out.push(node); + const nodeId = out.length - 1; + + // 2. cc.UITransform (K+1) + const uiCompInfo = makeCompPrefabInfo(); + out.push(uiCompInfo); + const uiCompInfoId = out.length - 1; + const uiTransform = makeUiTransform(nodeId, uiCompInfoId, size.width, size.height); + out.push(uiTransform); + const uiId = out.length - 1; + + // 3. 主组件 (K+3) — CompPrefabInfo 先 push (K+2), 主组件 __prefab → 该 CompPrefabInfo + const mainCompInfo = makeCompPrefabInfo(); + out.push(mainCompInfo); + const mainCompInfoId = out.length - 1; + + const spriteFrameUuid = (spirit.type === '0' || spirit.type === '3' || spirit.type === '5') + ? resolveSpriteFrameUuid(spirit.imgResID) + : null; + + let mainComp; + if (componentName === 'cc.Sprite') { + const spriteType = spirit.type === '5' ? 1 : 0; // 5=SLICED, else SIMPLE + mainComp = makeSprite(nodeId, mainCompInfoId, spriteFrameUuid, spriteType); + } else if (componentName === 'cc.Label') { + mainComp = makeLabel(nodeId, mainCompInfoId, spirit.name); + } else if (componentName === 'cc.ProgressBar') { + mainComp = makeProgressBar(nodeId, mainCompInfoId, spriteFrameUuid); + } else if (componentName === 'cc.EditBox') { + mainComp = makeEditBox(nodeId, mainCompInfoId, spriteFrameUuid, spirit.name); + } else { + // 数据源权威唯一: mapSpiritTypeToComponent 已 throw "unknown" 情况, 此处兜底是 + // 防御性 — 若新增组件类型但未在 makeXxx 注册, 显式报错暴露. + throw new Error(`emitPrefabJson: component "${componentName}" not yet supported by emitPrefabJson — add makeXxx helper for SpiritType ${spirit.type}`); + } + out.push(mainComp); + const mainCompId = out.length - 1; + + // 4. cc.Widget (K+5) — CompPrefabInfo 先 push (K+4) + const widgetCompInfo = makeCompPrefabInfo(); + out.push(widgetCompInfo); + const widgetCompInfoId = out.length - 1; + const widget = makeWidget(nodeId, widgetCompInfoId); + out.push(widget); + const widgetId = out.length - 1; + + // 5. cc.PrefabInfo (K+7) — Node 的 _prefab 指向这里 + const prefabInfo = makePrefabInfo(); + out.push(prefabInfo); + const prefabInfoId = out.length - 1; + + // ── Wire up Node ──────────────────────────────────────────── + node._components.push({ __id__: uiId }, { __id__: mainCompId }, { __id__: widgetId }); + node._prefab = { __id__: prefabInfoId }; + + return { node, nodeId, prefabInfoId }; +} + +/** + * 给非 Spirit 节点 (root / group-X) 追加末尾的 UITransform + Widget + PrefabInfo + * 块, 并把 node._components / node._prefab 补齐. + */ +function appendNodeEnvelope(node, nodeId, out) { + // 1. UITransform + CompPrefabInfo + const uiCompInfo = makeCompPrefabInfo(); + out.push(uiCompInfo); + const uiCompInfoId = out.length - 1; + const uiTransform = makeUiTransform(nodeId, uiCompInfoId, DESIGN_WIDTH, DESIGN_HEIGHT); + out.push(uiTransform); + const uiId = out.length - 1; + + // 2. Widget + CompPrefabInfo + const widgetCompInfo = makeCompPrefabInfo(); + out.push(widgetCompInfo); + const widgetCompInfoId = out.length - 1; + const widget = makeWidget(nodeId, widgetCompInfoId); + out.push(widget); + const widgetId = out.length - 1; + + // 3. PrefabInfo + const prefabInfo = makePrefabInfo(); + out.push(prefabInfo); + const prefabInfoId = out.length - 1; + + node._components.push({ __id__: uiId }, { __id__: widgetId }); + node._prefab = { __id__: prefabInfoId }; +}