diff --git a/client/js/01_SubGame/codes/ui/LayoutSolver.js b/client/js/01_SubGame/codes/ui/LayoutSolver.js index a5cf0b0..5aad44e 100644 --- a/client/js/01_SubGame/codes/ui/LayoutSolver.js +++ b/client/js/01_SubGame/codes/ui/LayoutSolver.js @@ -171,30 +171,64 @@ var EQW_LayoutSolver = EQW_LayoutSolver || { }, //attach:贴在另一个精灵上。corner 为空 = 在目标【内部】对齐;有值 = 与目标对应角重合 + //target 支持运行时注入:叫分档位贴自己的按钮、花色图标贴自己的按钮、牌角标贴"打出的那张牌", + //这类目标配置里写不出固定值,节点写成无 target 的模板,由 ctx.target 在运行时补上(ctx.target 优先于 n.target) _solveAttach: function (n, ctx) { if (!ctx.rects) { throw new Error('[EQW_LayoutSolver] attach 需要 ctx.rects'); } - var target = ctx.rects[n.target]; - if (!target) { throw new Error('[EQW_LayoutSolver] attach 的 target 未在 ctx.rects 中: ' + n.target); } + var targetKey = ctx.target || n.target; + if (!targetKey) { throw new Error('[EQW_LayoutSolver] attach 配置未写 target,且 ctx.target 未给出'); } + 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 }; var offset = { x: n.offsetX || 0, y: n.offsetY || 0 }; var pos; + //w/h 是否必填取决于本次实际走的对齐分支是否用到精灵尺寸(AlignmentUtils 源码逐分支核对): + //纯文字标签在 left/top/bottom 对齐下不需要尺寸,一刀切要求 w/h 会逼配置去编造假数值 if (n.corner) { switch (n.corner) { - case 'topLeft': pos = AlignmentUtils.alignCornerTopLeft(target, sprite, offset); break; - case 'topRight': pos = AlignmentUtils.alignCornerTopRight(target, sprite, offset); break; - case 'bottomLeft': pos = AlignmentUtils.alignCornerBottomLeft(target, sprite, offset); break; - case 'bottomRight': pos = AlignmentUtils.alignCornerBottomRight(target, sprite, offset); break; + case 'topLeft': + pos = AlignmentUtils.alignCornerTopLeft(target, sprite, offset); break; + case 'topRight': + this._requireW(n, targetKey, 'corner=topRight'); + pos = AlignmentUtils.alignCornerTopRight(target, sprite, offset); break; + case 'bottomLeft': + this._requireH(n, targetKey, 'corner=bottomLeft'); + pos = AlignmentUtils.alignCornerBottomLeft(target, sprite, offset); break; + case 'bottomRight': + this._requireW(n, targetKey, 'corner=bottomRight'); + this._requireH(n, 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); + } + //vAlign: 只有 middle 用 sprite.height;top/bottom 都不用(bottom 用的是 target.height,第132行) + if (n.vAlign === 'middle') { + this._requireH(n, 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 }]; }, + _requireW: function (n, targetKey, mode) { + if (typeof n.w !== 'number') { + throw new Error('[EQW_LayoutSolver] attach target=' + targetKey + '(' + mode + ')需要数字 w,当前缺失'); + } + }, + + _requireH: function (n, targetKey, mode) { + if (typeof n.h !== 'number') { + throw new Error('[EQW_LayoutSolver] attach target=' + targetKey + '(' + mode + ')需要数字 h,当前缺失'); + } + }, + //distribute* 只返回 {x,y},这里补上尺寸 _withSize: function (positions, w, h) { var out = []; diff --git a/client/tests/test_layoutsolver.js b/client/tests/test_layoutsolver.js index c4d428b..759fda5 100644 --- a/client/tests/test_layoutsolver.js +++ b/client/tests/test_layoutsolver.js @@ -145,6 +145,48 @@ t.eq('attach 角 bottomLeft 精确坐标', cBottomLeft, [{ x: 100, y: 230, width 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 }]); +// ============ attach:按实际对齐模式校验 w/h(缺失且用得到时抛错,用不到时不强制) ============ +// hAlign=right 用到 sprite.width(AlignmentUtils.js:111),缺 w 抛错;hAlign=left(101行)不用,缺 w 不抛错 +t.eq('attach hAlign=right 缺 w 抛错', + throws(() => EQW_LayoutSolver.solve({ kind: 'attach', target: 'TARGET', hAlign: 'right', vAlign: 'top', h: 10 }, { rects: rects })), true); +t.eq('attach hAlign=left 缺 w 不抛错', + throws(() => EQW_LayoutSolver.solve({ kind: 'attach', target: 'TARGET', hAlign: 'left', vAlign: 'top', h: 10 }, { rects: rects })), false); + +// vAlign=middle 用到 sprite.height(127行),缺 h 抛错;vAlign=top(122行)不用,缺 h 不抛错 +t.eq('attach vAlign=middle 缺 h 抛错', + throws(() => EQW_LayoutSolver.solve({ kind: 'attach', target: 'TARGET', hAlign: 'left', vAlign: 'middle', w: 20 }, { rects: rects })), true); +t.eq('attach vAlign=top 缺 h 不抛错', + throws(() => EQW_LayoutSolver.solve({ kind: 'attach', target: 'TARGET', hAlign: 'left', vAlign: 'top', w: 20 }, { rects: rects })), false); + +// vAlign=bottom(132行)用的是 target.height,不是 sprite.height,缺 h 不应抛错——这条容易想当然写错 +t.eq('attach vAlign=bottom 缺 h 不抛错', + throws(() => EQW_LayoutSolver.solve({ kind: 'attach', target: 'TARGET', hAlign: 'left', vAlign: 'bottom', w: 20 }, { rects: rects })), false); + +// corner=topLeft(266-272行)不用 w/h,都缺不抛错;corner=bottomRight(344-350行)两者都用,缺一即抛错 +t.eq('attach corner=topLeft 缺 w/h 不抛错', + throws(() => EQW_LayoutSolver.solve({ kind: 'attach', target: 'TARGET', corner: 'topLeft' }, { rects: rects })), false); +t.eq('attach corner=bottomRight 缺 w 抛错', + throws(() => EQW_LayoutSolver.solve({ kind: 'attach', target: 'TARGET', corner: 'bottomRight', h: 10 }, { rects: rects })), true); +t.eq('attach corner=bottomRight 缺 h 抛错', + throws(() => EQW_LayoutSolver.solve({ kind: 'attach', target: 'TARGET', corner: 'bottomRight', w: 20 }, { rects: rects })), true); + +// ============ attach target 运行时注入(ctx.target,供叫分档位/花色图标/牌角标等模板节点用)============ +const rects2 = { TARGET: { x: 100, y: 200, width: 80, height: 40 }, OTHER: { x: 5, y: 6, width: 10, height: 10 } }; + +// ctx.target 覆盖配置里写死的 target +const attachOverride = { kind: 'attach', target: 'TARGET', corner: 'topLeft' }; +const aOverride = EQW_LayoutSolver.solve(attachOverride, { rects: rects2, target: 'OTHER' }); +t.eq('ctx.target 覆盖配置 target', { x: aOverride[0].x, y: aOverride[0].y }, { x: 5, y: 6 }); + +// 配置无 target(模板节点)时,ctx.target 生效 +const attachTemplate = { kind: 'attach', corner: 'topLeft' }; +const aFromCtxOnly = EQW_LayoutSolver.solve(attachTemplate, { rects: rects, target: 'TARGET' }); +t.eq('配置无 target 时 ctx.target 生效', { x: aFromCtxOnly[0].x, y: aFromCtxOnly[0].y }, { x: 100, y: 200 }); + +// 配置与 ctx 都没给 target:抛错 +t.eq('target 与 ctx.target 都缺失抛错', + throws(() => EQW_LayoutSolver.solve(attachTemplate, { rects: rects })), true); + // ============ bySeat 合并:列出的覆盖、未列出的继承 ============ const bySeat = { kind: 'fan', direction: 'horizontal', itemWidth: 90, itemHeight: 155,