配置落地后暴露两个缺口:叫分档位/花色图标/牌角标等 attach 目标要运行时 才确定,配置写不出固定 target,需要 ctx.target 覆盖/补全(优先于 n.target, 两者皆无则抛错);纯文字标签没有固定 w/h,此前缺失时会静默算出 NaN 坐标。 改为按本次实际走的对齐分支(hAlign/vAlign/corner)逐一核对 AlignmentUtils 源码确认是否用到 sprite.width/height,只在用得到时才要求对应字段是数字, 不用得到时(如 left/top/bottom、topLeft)不强制,避免逼纯文字配置编造假值。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
241 lines
11 KiB
JavaScript
241 lines
11 KiB
JavaScript
///////////////////////////////////////////////////////////////
|
||
////////// 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 支持运行时注入:叫分档位贴自己的按钮、花色图标贴自己的按钮、牌角标贴"打出的那张牌",
|
||
//这类目标配置里写不出固定值,节点写成无 target 的模板,由 ctx.target 在运行时补上(ctx.target 优先于 n.target)
|
||
_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); }
|
||
|
||
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':
|
||
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 = [];
|
||
for (var i = 0; i < positions.length; i++) {
|
||
out.push({ x: positions[i].x, y: positions[i].y, width: w, height: h });
|
||
}
|
||
return out;
|
||
}
|
||
};
|