Files
erqiwang_youle/client/js/01_SubGame/codes/ui/LayoutSolver.js
T
joywayerandClaude Opus 5 91f0eb116c 二七王:LayoutSolver attach 支持运行时 target 注入,按对齐模式校验 w/h
配置落地后暴露两个缺口:叫分档位/花色图标/牌角标等 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>
2026-08-26 21:29:31 +08:00

241 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 支持运行时注入:叫分档位贴自己的按钮、花色图标贴自己的按钮、牌角标贴"打出的那张牌",
//这类目标配置里写不出固定值,节点写成无 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;
}
};