fix(ui-migration): 拉伸解耦背景判定 + objectIndex 内容级校验 + 缺图抛错回归
评审 Important 修复: 1. spec §2.4: flags.stretch/拉伸widget 适用于全部满屏对象(52个),不只是层内首个 的 backgroundNodeId(40个,判定规则不变)。此前实现把两者绑死,导致12个非首位 满屏遮罩(如"创建房间遮罩")套用三区锚点几何,进 Cocos 后会错位。 2. 真实数据护栏补内容级校验: 遍历 manifest.objectIndex 全部991条,核对每条 layerId 与源数据 BelongLayerID 一致——杀掉"全部条目指向同一层"这类只查条数 查不出的变异,这是子系统 C 定位 1773 处 set_self 的承重字段。 3. 补"引用不存在的图显式抛错"的回归用例——此前只有代码审查保护,真实数据恰好 不触发,没有测试防线;错误信息须可定位对象ID与图ID。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -50,11 +50,12 @@ test('convertLayer 识别背景:层内首个 + 满屏 → stretch', () => {
|
|||||||
assert.equal(bg.widget.horizontal, 'stretch');
|
assert.equal(bg.widget.horizontal, 'stretch');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('convertLayer 背景判定只认层内首个:非首位的满屏对象不算背景', () => {
|
test('convertLayer 背景判定只认层内首个,但拉伸适用于所有满屏对象(spec §2.4)', () => {
|
||||||
const s = tinySource();
|
const s = tinySource();
|
||||||
// 追加一个非首位、同样满屏的对象——用来钉住"首个 + 满屏"而非"只要满屏"的判定规则
|
// 追加一个非首位、同样满屏的对象(如"创建房间遮罩"那类)——
|
||||||
|
// backgroundNodeId 仍应只认层内首个,但 flags.stretch/widget 对所有满屏对象一视同仁
|
||||||
s.objects.set(4, {
|
s.objects.set(4, {
|
||||||
ObjectID: 4, ObjectType: 2, ObjectName: '第二张满屏图', Left: 0, Top: 0, Width: 1280, Height: 720,
|
ObjectID: 4, ObjectType: 2, ObjectName: '第二张满屏遮罩', Left: 0, Top: 0, Width: 1280, Height: 720,
|
||||||
BelongLayerID: 2, IndexOfLayer: 4, ImageFileID: 1, FrameStyle: 0, FrameIndex: 0,
|
BelongLayerID: 2, IndexOfLayer: 4, ImageFileID: 1, FrameStyle: 0, FrameIndex: 0,
|
||||||
L9: 0, T9: 0, R9: 0, B9: 0, GroupID: 5, VoiceFileID: 0, TimerInterval: 0, events: [],
|
L9: 0, T9: 0, R9: 0, B9: 0, GroupID: 5, VoiceFileID: 0, TimerInterval: 0, events: [],
|
||||||
});
|
});
|
||||||
@@ -64,7 +65,8 @@ test('convertLayer 背景判定只认层内首个:非首位的满屏对象不
|
|||||||
const doc = convertLayer(s, s.layers[0], imageBuckets(s));
|
const doc = convertLayer(s, s.layers[0], imageBuckets(s));
|
||||||
assert.equal(doc.backgroundNodeId, 1, '背景仍应是层内首个对象');
|
assert.equal(doc.backgroundNodeId, 1, '背景仍应是层内首个对象');
|
||||||
const second = doc.nodes.find((n) => n.id === 4);
|
const second = doc.nodes.find((n) => n.id === 4);
|
||||||
assert.equal(second.flags.stretch, false, '非首位的满屏对象不应被认作背景');
|
assert.equal(second.flags.stretch, true, '满屏对象都应拉伸,不论是否层内首个');
|
||||||
|
assert.equal(second.widget.horizontal, 'stretch');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('convertLayer 非背景对象用三区锚点与转换坐标', () => {
|
test('convertLayer 非背景对象用三区锚点与转换坐标', () => {
|
||||||
@@ -108,6 +110,26 @@ test('convertLayer 保留 legacy 承重字段', () => {
|
|||||||
assert.deepEqual(btn.legacy.events, ['mousedown']);
|
assert.deepEqual(btn.legacy.events, ['mousedown']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('convertLayer 对象引用不存在的图时显式抛错(不兜底),消息可定位对象与图', () => {
|
||||||
|
const s = tinySource();
|
||||||
|
s.objects.set(4, {
|
||||||
|
ObjectID: 4, ObjectType: 2, ObjectName: '缺图对象', Left: 10, Top: 10, Width: 50, Height: 50,
|
||||||
|
BelongLayerID: 2, IndexOfLayer: 4, ImageFileID: 999, FrameStyle: 0, FrameIndex: 0,
|
||||||
|
L9: 0, T9: 0, R9: 0, B9: 0, GroupID: 5, VoiceFileID: 0, TimerInterval: 0, events: [],
|
||||||
|
});
|
||||||
|
s.layers[0].objectIds.push(4);
|
||||||
|
s.groups[0].objectIds.push(4);
|
||||||
|
|
||||||
|
assert.throws(
|
||||||
|
() => convertLayer(s, s.layers[0], imageBuckets(s)),
|
||||||
|
(err) => {
|
||||||
|
assert.ok(err.message.includes('对象 4'), `错误信息应包含对象 ID: ${err.message}`);
|
||||||
|
assert.ok(err.message.includes('999'), `错误信息应包含图 ID: ${err.message}`);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('convertLayer 帧号越界:回退帧 1、标记 flags.frameIndexOutOfRange、收集 warning', () => {
|
test('convertLayer 帧号越界:回退帧 1、标记 flags.frameIndexOutOfRange、收集 warning', () => {
|
||||||
const s = tinySource();
|
const s = tinySource();
|
||||||
// 追加一张 frame_all=2 的图,对象却写着 FrameIndex=3——复现真实数据里 9/40 那类越界
|
// 追加一张 frame_all=2 的图,对象却写着 FrameIndex=3——复现真实数据里 9/40 那类越界
|
||||||
@@ -172,10 +194,23 @@ test('convertAll 的 manifest 含 ObjectID 全局索引(C 的承重依赖)',
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('convertAll 在真实数据上覆盖全部对象与界面(回归护栏)', () => {
|
test('convertAll 在真实数据上覆盖全部对象与界面(回归护栏)', () => {
|
||||||
const { layers, manifest, warnings } = convertAll(loadSource(REAL_SOURCE));
|
const source = loadSource(REAL_SOURCE);
|
||||||
|
const { layers, manifest, warnings } = convertAll(source);
|
||||||
assert.equal(layers.length, 55, '界面数');
|
assert.equal(layers.length, 55, '界面数');
|
||||||
const total = layers.reduce((s, l) => s + l.nodes.length, 0);
|
const total = layers.reduce((s, l) => s + l.nodes.length, 0);
|
||||||
assert.equal(total, 991, '节点总数必须等于对象总数——不得丢对象');
|
assert.equal(total, 991, '节点总数必须等于对象总数——不得丢对象');
|
||||||
assert.equal(Object.keys(manifest.objectIndex).length, 991, 'ObjectID 索引必须完整');
|
assert.equal(Object.keys(manifest.objectIndex).length, 991, 'ObjectID 索引必须完整');
|
||||||
assert.equal(warnings.length, 10, '已知的帧号越界对象必须恰为 10 个——数量漂移应被人工关注');
|
assert.equal(warnings.length, 10, '已知的帧号越界对象必须恰为 10 个——数量漂移应被人工关注');
|
||||||
|
|
||||||
|
// 内容级校验(不只是条数):objectIndex 每条的 layerId 必须等于该对象真实所属的层
|
||||||
|
// (source 里的 BelongLayerID)。全量遍历能杀掉"全部条目被写成同一个 layerId"
|
||||||
|
// 这类变异——55 个不同层里任何一层的对象被错误映射,都会在此处失败。
|
||||||
|
for (const [idStr, entry] of Object.entries(manifest.objectIndex)) {
|
||||||
|
const obj = source.objects.get(Number(idStr));
|
||||||
|
assert.equal(
|
||||||
|
entry.layerId,
|
||||||
|
obj.BelongLayerID,
|
||||||
|
`ObjectID ${idStr} 的 objectIndex.layerId(${entry.layerId}) 应等于其 BelongLayerID(${obj.BelongLayerID})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -55,7 +55,10 @@ function spriteBlock(obj, image, bucket, warnings) {
|
|||||||
return { block, outOfRange };
|
return { block, outOfRange };
|
||||||
}
|
}
|
||||||
|
|
||||||
function toNode(obj, source, imageBucketMap, isBackground, warnings) {
|
function toNode(obj, source, imageBucketMap, warnings) {
|
||||||
|
// 拉伸(四边置0)适用于全部满屏对象(spec §2.4: 实测 52 个),不论是否层内首个。
|
||||||
|
// backgroundNodeId(层内唯一)是另一件事:仅用于"首个+满屏"的背景判定,与此处解耦。
|
||||||
|
const stretch = isFullScreen(obj.Width, obj.Height);
|
||||||
const node = {
|
const node = {
|
||||||
id: obj.ObjectID,
|
id: obj.ObjectID,
|
||||||
name: obj.ObjectName,
|
name: obj.ObjectName,
|
||||||
@@ -63,9 +66,9 @@ function toNode(obj, source, imageBucketMap, isBackground, warnings) {
|
|||||||
position: toCocosPosition(obj.Left, obj.Top, obj.Width, obj.Height),
|
position: toCocosPosition(obj.Left, obj.Top, obj.Width, obj.Height),
|
||||||
size: { width: obj.Width, height: obj.Height },
|
size: { width: obj.Width, height: obj.Height },
|
||||||
siblingIndex: obj.IndexOfLayer,
|
siblingIndex: obj.IndexOfLayer,
|
||||||
widget: isBackground ? stretchWidget() : inferWidget(obj.Left, obj.Width),
|
widget: stretch ? stretchWidget() : inferWidget(obj.Left, obj.Width),
|
||||||
flags: {
|
flags: {
|
||||||
stretch: isBackground,
|
stretch,
|
||||||
offscreen: isOffscreen(obj.Left, obj.Top, obj.Width, obj.Height),
|
offscreen: isOffscreen(obj.Left, obj.Top, obj.Width, obj.Height),
|
||||||
animation: obj.FrameStyle === 1,
|
animation: obj.FrameStyle === 1,
|
||||||
frameIndexOutOfRange: false,
|
frameIndexOutOfRange: false,
|
||||||
@@ -111,7 +114,7 @@ export function convertLayer(source, layer, imageBucketMap) {
|
|||||||
|
|
||||||
const warnings = [];
|
const warnings = [];
|
||||||
const nodes = objs
|
const nodes = objs
|
||||||
.map((o) => toNode(o, source, imageBucketMap, o.ObjectID === backgroundNodeId, warnings))
|
.map((o) => toNode(o, source, imageBucketMap, warnings))
|
||||||
.sort((a, b) => a.siblingIndex - b.siblingIndex);
|
.sort((a, b) => a.siblingIndex - b.siblingIndex);
|
||||||
|
|
||||||
const nodeIdSet = new Set(nodes.map((n) => n.id));
|
const nodeIdSet = new Set(nodes.map((n) => n.id));
|
||||||
|
|||||||
Reference in New Issue
Block a user