Files
erqiwang_youle/client/js/01_SubGame/codes/ui/LayoutSolver.js
T
joywayerandClaude Opus 5 6390ce74c2 二七王: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>
2026-08-26 21:43:33 +08:00

246 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
///////////////////////////////////////////////////////////////
////////// EQW_LayoutSolver: 五型布局求解器 ////////////////////
///////////////////////////////////////////////////////////////
// 清单 §6:布局配置是【纯数据】,一切求解逻辑集中在本文件。
// point 定点 line 等距排列(可 items 逐项不等宽)
// fan 自适应压缩 grid 网格 attach 相对贴附
//
// solve(node, ctx) 是【纯函数】:不碰精灵、不查引擎,故可完整单测。
// node: 一份布局配置(五型之一,可含 bySeat)
// ctx : { seat, count, rects }
// seat —— 'SELF'|'LEFT'|'RIGHT',仅 bySeat 需要
// count —— 运行时项数(清单 §6.1:count 由数据给出、不进配置)
// rects —— { 键名: {x,y,width,height} },仅 attach 需要
// 返回恒为数组,point / attach 长度为 1。
//
// 参数名一律沿用框架 AlignmentUtils 的入参名(清单 §6.0),配置可原样喂入。
// 精灵锚点恒在左上角,配置里的 anchorX/anchorY 是【对齐基准点】,不是精灵左上角。
// 出错一律抛异常,不返回 {x:0,y:0} 之类的兜底值(工程总则 §7 显式失败)。
var EQW_LayoutSolver = EQW_LayoutSolver || {
//——— 对外:求解 ———
solve: function (node, ctx) {
if (!node) { throw new Error('[EQW_LayoutSolver] node 为空'); }
ctx = ctx || {};
var n = this._mergeBySeat(node, ctx.seat);
switch (n.kind) {
case 'point': return this._solvePoint(n);
case 'line': return this._solveLine(n, ctx);
case 'fan': return this._solveFan(n, ctx);
case 'grid': return this._solveGrid(n, ctx);
case 'attach': return this._solveAttach(n, ctx);
default: throw new Error('[EQW_LayoutSolver] 未知 kind: ' + n.kind);
}
},
//——— 对外:求解并摆精灵(唯一触碰 SpriteManager 之处)———
apply: function (node, spriteIds, ctx) {
var rects = this.solve(node, ctx);
for (var i = 0; i < spriteIds.length && i < rects.length; i++) {
SpriteManager.setPosition(spriteIds[i], rects[i].x, rects[i].y);
}
return rects;
},
//——— bySeat 合并:bySeat[seat] 覆盖外层 base,未列出的字段继承(清单 §6.2)———
_mergeBySeat: function (node, seat) {
if (!node.bySeat) { return node; }
if (!seat) { throw new Error('[EQW_LayoutSolver] 配置含 bySeat,但 ctx.seat 未给出'); }
if (!node.bySeat[seat]) { throw new Error('[EQW_LayoutSolver] bySeat 无此显示位: ' + seat); }
var merged = {};
var k;
for (k in node) {
if (node.hasOwnProperty(k) && k !== 'bySeat') { merged[k] = node[k]; }
}
var v = node.bySeat[seat];
for (k in v) {
if (v.hasOwnProperty(k)) { merged[k] = v[k]; }
}
return merged;
},
_solvePoint: function (n) {
return [{ x: n.x, y: n.y, width: n.w, height: n.h }];
},
//line:等距排列。带 items 时逐项取宽(itemWidth 失效),此时不能走 distribute(它假设等宽)
_solveLine: function (n, ctx) {
if (n.items) { return this._solveLineItems(n); }
var count = ctx.count;
if (typeof count !== 'number') { throw new Error('[EQW_LayoutSolver] line 需要 ctx.count'); }
if (count <= 0) { return []; }
var positions = AlignmentUtils.distribute({
direction: n.direction,
anchorX: n.anchorX,
anchorY: n.anchorY,
count: count,
itemWidth: n.itemWidth,
itemHeight: n.itemHeight,
spacing: n.spacing || 0,
anchor: n.anchor
});
return this._withSize(positions, n.itemWidth, n.itemHeight);
},
//line + items:逐项不等宽(底栏功能钮组、埋牌操作条等,清单 §6.1)
_solveLineItems: function (n) {
var items = n.items;
var spacing = n.spacing || 0;
var i, total = 0;
for (i = 0; i < items.length; i++) { total += items[i].width; }
total += (items.length - 1) * spacing;
var start;
switch (n.anchor) {
case 'left': start = n.anchorX; break;
case 'right': start = n.anchorX - total; break;
default: start = n.anchorX - total / 2; break;
}
var out = [];
var cursor = start;
for (i = 0; i < items.length; i++) {
out.push({ x: cursor, y: n.anchorY, width: items[i].width, height: n.itemHeight });
cursor += items[i].width + spacing;
}
return out;
},
//fan:张数可变、总宽受限、动态压缩间距(清单 §6.1,间距算法【唯一实现】)
_solveFan: function (n, ctx) {
var count = ctx.count;
if (typeof count !== 'number') { throw new Error('[EQW_LayoutSolver] fan 需要 ctx.count'); }
if (count <= 0) { return []; }
var spacing = 0;
if (count > 1) {
var raw = (n.maxWidth - n.itemWidth) / (count - 1) - n.itemWidth;
spacing = Math.max(n.spacingMin, Math.min(n.spacingMax, raw));
}
var positions = AlignmentUtils.distribute({
direction: n.direction,
anchorX: n.anchorX,
anchorY: n.anchorY,
count: count,
itemWidth: n.itemWidth,
itemHeight: n.itemHeight,
spacing: spacing,
anchor: n.anchor
});
return this._withSize(positions, n.itemWidth, n.itemHeight);
},
//grid:固定行列的按钮阵,逐行调 distributeHorizontally(清单 §6.1)
_solveGrid: function (n, ctx) {
//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 []; }
var out = [];
for (var row = 0; row < n.rows && out.length < count; row++) {
var remain = count - out.length;
var inRow = Math.min(n.cols, remain);
var positions = AlignmentUtils.distributeHorizontally({
anchorX: n.anchorX,
anchorY: n.anchorY + row * (n.itemHeight + (n.spacingY || 0)),
count: n.cols, //按满格算位置,保证不足一行时列位不漂移
itemWidth: n.itemWidth,
itemHeight: n.itemHeight,
spacing: n.spacingX || 0,
anchor: n.anchor
});
for (var c = 0; c < inRow; c++) {
out.push({ x: positions[c].x, y: positions[c].y, width: n.itemWidth, height: n.itemHeight });
}
}
return out;
},
//attach:贴在另一个精灵上。corner 为空 = 在目标【内部】对齐;有值 = 与目标对应角重合
//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;
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); }
//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;
//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':
this._requireW(w, targetKey, 'corner=topRight');
pos = AlignmentUtils.alignCornerTopRight(target, sprite, offset); break;
case 'bottomLeft':
this._requireH(h, targetKey, 'corner=bottomLeft');
pos = AlignmentUtils.alignCornerBottomLeft(target, sprite, offset); break;
case '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(w, targetKey, 'hAlign=' + n.hAlign);
}
//vAlign: 只有 middle 用 sprite.height;top/bottom 都不用(bottom 用的是 target.height,第132行)
if (n.vAlign === 'middle') {
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: w, height: h }];
},
_requireW: function (w, targetKey, mode) {
if (typeof w !== 'number') {
throw new Error('[EQW_LayoutSolver] attach target=' + targetKey + '(' + mode + ')需要数字 w:配置未写 w,且 ctx.w 未给出');
}
},
_requireH: function (h, targetKey, mode) {
if (typeof h !== 'number') {
throw new Error('[EQW_LayoutSolver] attach target=' + targetKey + '(' + mode + ')需要数字 h:配置未写 h,且 ctx.h 未给出');
}
},
//distribute* 只返回 {x,y},这里补上尺寸
_withSize: function (positions, w, h) {
var out = [];
for (var i = 0; i < positions.length; i++) {
out.push({ x: positions[i].x, y: positions[i].y, width: w, height: h });
}
return out;
}
};