Files
youle_cocos/cocoscreator_projects/scripts/ui-migration/convert.mjs
T
joywayerandClaude Opus 5 b6c9af25e5 feat(ui-migration): 中间描述组装
对象 → 节点(坐标/锚点/九宫格/帧引用/flags/legacy), 层 → LayerDoc(bucket/背景/分组)。
manifest 含 ObjectID 全局索引 —— 子系统 C 移植 1773 处 set_self 调用的承重依赖。
真实数据回归护栏: 55 界面 / 991 节点, 一个对象都不得丢。

真实数据里有 10 个对象(如 3067/3068/3075/3076/3087/3088/3089/3145/388/3265)的
FrameIndex 超出其图的帧数(多帧图重导后帧数变少但对象未同步)——不抛错中止、不静默
钳到末帧,而是回退帧1 + 打 flags.frameIndexOutOfRange + 收进 convertAll 返回的
warnings 清单(objectId/layerId/imageFileId/frameIndex/frameCount/reason),真实
数据护栏钉死 warnings.length===10,数量漂移会被人工看到。另补一条背景判定用例:
非首位的满屏对象不应被误判为背景,钉住"层内首个+满屏"而非"只要满屏"的规则。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 01:29:06 +08:00

159 lines
5.1 KiB
JavaScript
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.
import {
DESIGN_WIDTH, DESIGN_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, isBackground, warnings) {
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: isBackground ? stretchWidget() : inferWidget(obj.Left, obj.Width),
flags: {
stretch: isBackground,
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, o.ObjectID === backgroundNodeId, 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,
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 };
}