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>
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user