import { DESIGN_WIDTH, DESIGN_HEIGHT, CANVAS_WIDTH, CANVAS_HEIGHT, toCocosPosition, inferWidget, stretchWidget, isFullScreen, isOffscreen, } from './coords.mjs'; import { frameFileName, padWidth } from './frames.mjs'; import { layerBucket, imageBuckets } from './buckets.mjs'; /** 中间描述组装。见 spec §1。 */ const OBJECT_TYPE_SPRITE = 2; const OBJECT_TYPE_TEXT = 4; /** * 帧号越界(旧数据里美术重导时帧数变少,但对象上的 FrameIndex 没跟着更新)。 * 只拦这一种已知情况——预判而非 try/catch,免得连带吞掉 frameFileName 里真正的实现 bug。 * 单帧图(frame_all<=1)的 FrameIndex 本就与渲染无关,不算越界。 */ function isFrameIndexOutOfRange(obj, image) { const requested = obj.FrameIndex || 1; return image.frame_all > 1 && requested > image.frame_all; } function spriteBlock(obj, image, bucket, warnings) { const dir = `atlas-${bucket}`; const sliced = obj.L9 || obj.T9 || obj.R9 || obj.B9; const outOfRange = isFrameIndexOutOfRange(obj, image); const frameIndex = outOfRange ? 1 : (obj.FrameIndex || 1); if (outOfRange) { warnings.push({ objectId: obj.ObjectID, layerId: obj.BelongLayerID, imageFileId: obj.ImageFileID, frameIndex: obj.FrameIndex, frameCount: image.frame_all, reason: `对象 ${obj.ObjectID}(${obj.ObjectName})引用 ${image.bmp} 的帧 ${obj.FrameIndex},` + `超出该图帧数 ${image.frame_all}(很可能是美术重导后 FrameIndex 未同步更新),已回退为帧 1`, }); } const block = { frame: `${dir}/${frameFileName(image.bmp, frameIndex, image.frame_all)}`, type: sliced ? 'sliced' : 'simple', }; if (image.frame_all > 1) { block.frameSet = { prefix: `${dir}/${image.bmp.replace(/\.png$/i, '')}`, count: image.frame_all, pad: padWidth(image.frame_all), }; } if (sliced) { block.slice = { left: obj.L9, right: obj.R9, top: obj.T9, bottom: obj.B9 }; } return { block, outOfRange }; } function toNode(obj, source, imageBucketMap, warnings) { // 拉伸(四边置0)适用于全部满屏对象(spec §2.4: 实测 52 个),不论是否层内首个。 // backgroundNodeId(层内唯一)是另一件事:仅用于"首个+满屏"的背景判定,与此处解耦。 const stretch = isFullScreen(obj.Width, obj.Height); const node = { id: obj.ObjectID, name: obj.ObjectName, kind: obj.ObjectType === OBJECT_TYPE_TEXT ? 'label' : 'sprite', position: toCocosPosition(obj.Left, obj.Top, obj.Width, obj.Height), size: { width: obj.Width, height: obj.Height }, siblingIndex: obj.IndexOfLayer, widget: stretch ? stretchWidget() : inferWidget(obj.Left, obj.Width), flags: { stretch, offscreen: isOffscreen(obj.Left, obj.Top, obj.Width, obj.Height), animation: obj.FrameStyle === 1, frameIndexOutOfRange: false, }, legacy: { objectId: obj.ObjectID, imageFileId: obj.ImageFileID ?? 0, frameIndex: obj.FrameIndex ?? 0, frameStyle: obj.FrameStyle ?? 0, groupId: obj.GroupID ?? 0, voiceFileId: obj.VoiceFileID ?? 0, timerInterval: obj.TimerInterval ?? 0, left: obj.Left, top: obj.Top, events: obj.events ?? [], }, }; if (node.kind === 'sprite' && obj.ImageFileID) { const image = source.images.get(obj.ImageFileID); if (!image) { throw new Error( `对象 ${obj.ObjectID}(${obj.ObjectName})引用了不存在的图 ImageFileID=${obj.ImageFileID}`, ); } const { block, outOfRange } = spriteBlock( obj, image, imageBucketMap.get(obj.ImageFileID) ?? 'unassigned', warnings, ); node.sprite = block; node.flags.frameIndexOutOfRange = outOfRange; } return node; } export function convertLayer(source, layer, imageBucketMap) { const objs = layer.objectIds .map((id) => source.objects.get(id)) .filter(Boolean); const first = objs[0]; const backgroundNodeId = first && isFullScreen(first.Width, first.Height) ? first.ObjectID : null; const warnings = []; const nodes = objs .map((o) => toNode(o, source, imageBucketMap, warnings)) .sort((a, b) => a.siblingIndex - b.siblingIndex); const nodeIdSet = new Set(nodes.map((n) => n.id)); const groups = source.groups .map((g) => ({ groupId: g.groupId, nodeIds: g.objectIds.filter((id) => nodeIdSet.has(id)) })) .filter((g) => g.nodeIds.length > 0); return { layerId: layer.layerId, name: layer.name, bucket: layerBucket(layer.layerId, layer.name), backgroundNodeId, groups, nodes, warnings, }; } export function convertAll(source) { const imageBucketMap = imageBuckets(source); const layers = source.layers.map((l) => convertLayer(source, l, imageBucketMap)); const objectIndex = {}; const warnings = []; for (const doc of layers) { for (const n of doc.nodes) objectIndex[String(n.id)] = { layerId: doc.layerId, nodeId: n.id }; warnings.push(...doc.warnings); } const manifest = { generatedBy: 'scripts/convert-ui.mjs', // 内容区基准(旧数据事实),坐标/三区据此判定,不随设计分辨率改变。 designWidth: DESIGN_WIDTH, designHeight: DESIGN_HEIGHT, // 设计分辨率画布(20:9),落地 Layer/Group 根节点尺寸据此设定。 canvasWidth: CANVAS_WIDTH, canvasHeight: CANVAS_HEIGHT, layers: layers.map((d) => ({ layerId: d.layerId, name: d.name, bucket: d.bucket, file: `${String(d.layerId).padStart(3, '0')}-${d.name}.json`, nodeCount: d.nodes.length, })), objectIndex, }; return { layers, manifest, warnings }; }