二七王:LayoutSolver attach 加 ctx.w/ctx.h 运行时尺寸注入通道

求解冒烟守卫拿真实布局配置跑出 9 个失败节点:叫分档位分数文字、花色数量
文字、牌角标、结算文案等动态文字标签配置里有意不写 w/h(真实宽高要等
文案渲染出来才知道),此前的按对齐模式校验因此在 hAlign=center/right、
corner=topRight/bottomRight 时正确地抛错,但没给调用方注入真实尺寸的通道。

补 ctx.w/ctx.h,语义与已有的 ctx.target 对称:优先于配置里的 n.w/n.h,
校验与求解都用合并后的值,返回的 width/height 也用合并后的值(不再是
可能为 undefined 的配置原值);两处都缺且用得到该维度时仍抛错,错误信息
点明"配置未写 X,且 ctx.X 未给出"两条路径。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-26 21:43:33 +08:00
co-authored by Claude Opus 5
parent ec954ea4fe
commit 6390ce74c2
2 changed files with 56 additions and 16 deletions
+21 -16
View File
@@ -171,8 +171,9 @@ var EQW_LayoutSolver = EQW_LayoutSolver || {
},
//attach:贴在另一个精灵上。corner 为空 = 在目标【内部】对齐;有值 = 与目标对应角重合
//target 支持运行时注入:叫分档位贴自己的按钮、花色图标贴自己的按钮、牌角标贴"打出的那张牌",
//这类目标配置里写不出固定值,节点写成无 target 的模板,由 ctx.target 在运行时补上(ctx.target 优先于 n.target)
//target/w/h 都支持运行时注入:叫分档位贴自己的按钮、花色图标贴自己的按钮、牌角标贴"打出的那张牌",
//这类目标配置里写不出固定值;动态文字标签的真实宽高要等文案渲染出来才知道,配置里也留空。
//这些节点写成模板,由 ctx.target/ctx.w/ctx.h 在运行时补上(ctx.* 优先于 n.* 里的同名字段)
_solveAttach: function (n, ctx) {
if (!ctx.rects) { throw new Error('[EQW_LayoutSolver] attach 需要 ctx.rects'); }
var targetKey = ctx.target || n.target;
@@ -180,7 +181,11 @@ var EQW_LayoutSolver = EQW_LayoutSolver || {
var target = ctx.rects[targetKey];
if (!target) { throw new Error('[EQW_LayoutSolver] attach 的 target 未在 ctx.rects 中: ' + targetKey); }
var sprite = { width: n.w, height: n.h };
//ctx.w/ctx.h 优先于配置里的 n.w/n.h;后续的校验、求解、返回值一律用合并后的值
var w = (typeof ctx.w === 'number') ? ctx.w : n.w;
var h = (typeof ctx.h === 'number') ? ctx.h : n.h;
var sprite = { width: w, height: h };
var offset = { x: n.offsetX || 0, y: n.offsetY || 0 };
var pos;
@@ -191,41 +196,41 @@ var EQW_LayoutSolver = EQW_LayoutSolver || {
case 'topLeft':
pos = AlignmentUtils.alignCornerTopLeft(target, sprite, offset); break;
case 'topRight':
this._requireW(n, targetKey, 'corner=topRight');
this._requireW(w, targetKey, 'corner=topRight');
pos = AlignmentUtils.alignCornerTopRight(target, sprite, offset); break;
case 'bottomLeft':
this._requireH(n, targetKey, 'corner=bottomLeft');
this._requireH(h, targetKey, 'corner=bottomLeft');
pos = AlignmentUtils.alignCornerBottomLeft(target, sprite, offset); break;
case 'bottomRight':
this._requireW(n, targetKey, 'corner=bottomRight');
this._requireH(n, targetKey, 'corner=bottomRight');
this._requireW(w, targetKey, 'corner=bottomRight');
this._requireH(h, targetKey, 'corner=bottomRight');
pos = AlignmentUtils.alignCornerBottomRight(target, sprite, offset); break;
default: throw new Error('[EQW_LayoutSolver] 未知 corner: ' + n.corner);
}
} else {
//hAlign: center/right 要用 sprite.width;left 不用(AlignmentUtils.js 第106/111/101行)
if (n.hAlign === 'center' || n.hAlign === 'right') {
this._requireW(n, targetKey, 'hAlign=' + n.hAlign);
this._requireW(w, targetKey, 'hAlign=' + n.hAlign);
}
//vAlign: 只有 middle 用 sprite.height;top/bottom 都不用(bottom 用的是 target.height,第132行)
if (n.vAlign === 'middle') {
this._requireH(n, targetKey, 'vAlign=' + n.vAlign);
this._requireH(h, targetKey, 'vAlign=' + n.vAlign);
}
pos = AlignmentUtils.alignTo(target, sprite, n.hAlign, n.vAlign, offset);
}
return [{ x: pos.x, y: pos.y, width: n.w, height: n.h }];
return [{ x: pos.x, y: pos.y, width: w, height: h }];
},
_requireW: function (n, targetKey, mode) {
if (typeof n.w !== 'number') {
throw new Error('[EQW_LayoutSolver] attach target=' + targetKey + '(' + mode + ')需要数字 w,当前缺失');
_requireW: function (w, targetKey, mode) {
if (typeof w !== 'number') {
throw new Error('[EQW_LayoutSolver] attach target=' + targetKey + '(' + mode + ')需要数字 w:配置未写 w,且 ctx.w 未给出');
}
},
_requireH: function (n, targetKey, mode) {
if (typeof n.h !== 'number') {
throw new Error('[EQW_LayoutSolver] attach target=' + targetKey + '(' + mode + ')需要数字 h,当前缺失');
_requireH: function (h, targetKey, mode) {
if (typeof h !== 'number') {
throw new Error('[EQW_LayoutSolver] attach target=' + targetKey + '(' + mode + ')需要数字 h:配置未写 h,且 ctx.h 未给出');
}
},
+35
View File
@@ -170,6 +170,41 @@ t.eq('attach corner=bottomRight 缺 w 抛错',
t.eq('attach corner=bottomRight 缺 h 抛错',
throws(() => EQW_LayoutSolver.solve({ kind: 'attach', target: 'TARGET', corner: 'bottomRight', w: 20 }, { rects: rects })), true);
// ============ attach w/h 运行时注入(ctx.w/ctx.h,供动态文字标签/牌角标等尺寸未知的模板节点用)============
// 配置无 w、hAlign=right(用得到 sprite.width)、靠 ctx.w 补上 → 正常求解,返回 width 就是 ctx.w
const wFromCtx = EQW_LayoutSolver.solve(
{ kind: 'attach', target: 'TARGET', hAlign: 'right', vAlign: 'top', h: 10 },
{ rects: rects, w: 42 }
);
t.eq('ctx.w 补全缺失的 w,正常求解', throws(() => EQW_LayoutSolver.solve(
{ kind: 'attach', target: 'TARGET', hAlign: 'right', vAlign: 'top', h: 10 }, { rects: rects, w: 42 }
)), false);
t.eq('返回的 width 等于 ctx.w', wFromCtx[0].width, 42);
// 配置有 w,同时给 ctx.w → ctx.w 优先覆盖配置值
const wOverride = EQW_LayoutSolver.solve(
{ kind: 'attach', target: 'TARGET', hAlign: 'right', vAlign: 'top', w: 20, h: 10 },
{ rects: rects, w: 99 }
);
t.eq('ctx.w 覆盖配置里的 w', wOverride[0].width, 99);
// 配置无 h、vAlign=middle(用得到 sprite.height)、靠 ctx.h 补上 → 正常求解,返回 height 就是 ctx.h
const hFromCtx = EQW_LayoutSolver.solve(
{ kind: 'attach', target: 'TARGET', hAlign: 'left', vAlign: 'middle', w: 20 },
{ rects: rects, h: 33 }
);
t.eq('返回的 height 等于 ctx.h', hFromCtx[0].height, 33);
// 配置无 w、hAlign=right、ctx.w 也没给 → 仍然抛错(不能因为加了通道就失效)
t.eq('两处都没给 w 仍抛错', throws(() => EQW_LayoutSolver.solve(
{ kind: 'attach', target: 'TARGET', hAlign: 'right', vAlign: 'top', h: 10 }, { rects: rects }
)), true);
// ctx.w/ctx.h 对不需要该维度的对齐模式(hAlign=left)不产生影响,仍正常求解
t.eq('hAlign=left 时给不给 ctx.w 都不影响', throws(() => EQW_LayoutSolver.solve(
{ kind: 'attach', target: 'TARGET', hAlign: 'left', vAlign: 'top' }, { rects: rects, w: 7 }
)), false);
// ============ attach target 运行时注入(ctx.target,供叫分档位/花色图标/牌角标等模板节点用)============
const rects2 = { TARGET: { x: 100, y: 200, width: 80, height: 40 }, OTHER: { x: 5, y: 6, width: 10, height: 10 } };