二七王:LayoutSolver grid 补显式失败校验,attach 四角补精确坐标测试

审查发现 _solveGrid 在 count > cols*rows 时静默截断多出的项,违反显式失败
红线;补校验直接抛错并报出 count 与容量。fillOrder 字段之前从未被读取、
形同虚设,改为显式拒绝除 'row' 外的值(不实现列优先,YAGNI)。
测试补 grid 超载/恰好满格/fillOrder 三态,以及 attach 四角(topLeft/
bottomLeft/bottomRight)的精确坐标核对(此前只有 topRight 有精确值)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-26 20:15:57 +08:00
co-authored by Claude Opus 5
parent 2d0469e7f3
commit 8d6d272f76
2 changed files with 44 additions and 2 deletions
+11 -1
View File
@@ -137,7 +137,17 @@ var EQW_LayoutSolver = EQW_LayoutSolver || {
//grid:固定行列的按钮阵,逐行调 distributeHorizontally(清单 §6.1) //grid:固定行列的按钮阵,逐行调 distributeHorizontally(清单 §6.1)
_solveGrid: function (n, ctx) { _solveGrid: function (n, ctx) {
var count = (typeof ctx.count === 'number') ? ctx.count : (n.cols * n.rows); //fillOrder 目前只实现行优先;不写等同 row,写了别的值一律显式拒绝(YAGNI,不做无人用的列优先)
if (n.fillOrder && n.fillOrder !== 'row') {
throw new Error('[EQW_LayoutSolver] grid 仅支持 fillOrder=row,暂不支持: ' + n.fillOrder);
}
var capacity = n.cols * n.rows;
var count = (typeof ctx.count === 'number') ? ctx.count : capacity;
//配置与运行时项数不一致时必须显式报错,不能静默截断多出的项(工程总则 §7 显式失败)
if (count > capacity) {
throw new Error('[EQW_LayoutSolver] grid 项数超出容量: count=' + count + ' > cols(' + n.cols + ')*rows(' + n.rows + ')=' + capacity);
}
if (count <= 0) { return []; } if (count <= 0) { return []; }
var out = []; var out = [];
+33 -1
View File
@@ -87,6 +87,21 @@ t.eq('grid 第8项 y', g[7].y, 160 + 56 + 24);
// count 少于满格时只出 count 个 // count 少于满格时只出 count 个
t.eq('grid 不足一行', EQW_LayoutSolver.solve(grid, { count: 3 }).length, 3); t.eq('grid 不足一行', EQW_LayoutSolver.solve(grid, { count: 3 }).length, 3);
// 边界:count 恰好等于容量(cols*rows=14)时正常返回,不抛错
t.eq('grid 恰好满格不抛错', EQW_LayoutSolver.solve(grid, { count: 14 }).length, 14);
// count 超出容量:显式抛错,不静默截断多出的项
t.eq('grid 超载抛错', throws(() => EQW_LayoutSolver.solve(grid, { count: 15 })), true);
// fillOrder 校验:目前只支持行优先(YAGNI,未实现列优先),不写等同 row
const gridNoFillOrder = { kind: 'grid', anchorX: 640, anchorY: 160, cols: 7, rows: 2,
itemWidth: 74, itemHeight: 56, spacingX: 7, spacingY: 24, anchor: 'center' };
t.eq('grid 不写 fillOrder 正常', EQW_LayoutSolver.solve(gridNoFillOrder, { count: 14 }).length, 14);
t.eq('grid fillOrder=row 正常', EQW_LayoutSolver.solve(grid, { count: 14 }).length, 14);
const gridColumnOrder = { kind: 'grid', anchorX: 640, anchorY: 160, cols: 7, rows: 2,
itemWidth: 74, itemHeight: 56, spacingX: 7, spacingY: 24, anchor: 'center', fillOrder: 'column' };
t.eq('grid fillOrder=column 抛错', throws(() => EQW_LayoutSolver.solve(gridColumnOrder, { count: 14 })), true);
// ============ attach 内部对齐(corner 为空)============ // ============ attach 内部对齐(corner 为空)============
const rects = { TARGET: { x: 100, y: 200, width: 80, height: 40 } }; const rects = { TARGET: { x: 100, y: 200, width: 80, height: 40 } };
const attachIn = { kind: 'attach', target: 'TARGET', hAlign: 'right', vAlign: 'top', const attachIn = { kind: 'attach', target: 'TARGET', hAlign: 'right', vAlign: 'top',
@@ -107,12 +122,29 @@ const attachCorner = { kind: 'attach', target: 'TARGET', corner: 'topRight',
const a2 = EQW_LayoutSolver.solve(attachCorner, { rects: rects }); const a2 = EQW_LayoutSolver.solve(attachCorner, { rects: rects });
t.eq('attach 外侧右上角', a2, [{ x: 100 + 80 - 20 + 4, y: 200 + 6, width: 20, height: 10 }]); t.eq('attach 外侧右上角', a2, [{ x: 100 + 80 - 20 + 4, y: 200 + 6, width: 20, height: 10 }]);
// 四个角都要能解 // 四个角都要能解(通用检查:长度与类型)
['topLeft', 'topRight', 'bottomLeft', 'bottomRight'].forEach(c => { ['topLeft', 'topRight', 'bottomLeft', 'bottomRight'].forEach(c => {
const r = EQW_LayoutSolver.solve({ kind: 'attach', target: 'TARGET', corner: c, w: 20, h: 10 }, { rects: rects }); const r = EQW_LayoutSolver.solve({ kind: 'attach', target: 'TARGET', corner: c, w: 20, h: 10 }, { rects: rects });
t.eq('attach 角 ' + c + ' 可解', r.length === 1 && typeof r[0].x === 'number', true); t.eq('attach 角 ' + c + ' 可解', r.length === 1 && typeof r[0].x === 'number', true);
}); });
// 四角精确坐标核对:target={x:100,y:200,width:80,height:40},sprite={w:20,h:10},offset 缺省=0
// topLeft(AlignmentUtils.js:266-272):x=target.x=100,y=target.y=200
const cTopLeft = EQW_LayoutSolver.solve({ kind: 'attach', target: 'TARGET', corner: 'topLeft', w: 20, h: 10 }, { rects: rects });
t.eq('attach 角 topLeft 精确坐标', cTopLeft, [{ x: 100, y: 200, width: 20, height: 10 }]);
// topRight(AlignmentUtils.js:292-298):x=target.x+target.width-sprite.width=100+80-20=160,y=target.y=200
const cTopRight = EQW_LayoutSolver.solve({ kind: 'attach', target: 'TARGET', corner: 'topRight', w: 20, h: 10 }, { rects: rects });
t.eq('attach 角 topRight 精确坐标', cTopRight, [{ x: 160, y: 200, width: 20, height: 10 }]);
// bottomLeft(AlignmentUtils.js:318-324):x=target.x=100,y=target.y+target.height-sprite.height=200+40-10=230
const cBottomLeft = EQW_LayoutSolver.solve({ kind: 'attach', target: 'TARGET', corner: 'bottomLeft', w: 20, h: 10 }, { rects: rects });
t.eq('attach 角 bottomLeft 精确坐标', cBottomLeft, [{ x: 100, y: 230, width: 20, height: 10 }]);
// bottomRight(AlignmentUtils.js:344-350):x=target.x+target.width-sprite.width=100+80-20=160,y=同 bottomLeft=230
const cBottomRight = EQW_LayoutSolver.solve({ kind: 'attach', target: 'TARGET', corner: 'bottomRight', w: 20, h: 10 }, { rects: rects });
t.eq('attach 角 bottomRight 精确坐标', cBottomRight, [{ x: 160, y: 230, width: 20, height: 10 }]);
// ============ bySeat 合并:列出的覆盖、未列出的继承 ============ // ============ bySeat 合并:列出的覆盖、未列出的继承 ============
const bySeat = { const bySeat = {
kind: 'fan', direction: 'horizontal', itemWidth: 90, itemHeight: 155, kind: 'fan', direction: 'horizontal', itemWidth: 90, itemHeight: 155,