二七王:前端 LayoutSolver,五型布局求解器
solve 是纯函数(attach 靠 ctx.rects 查目标矩形而非反查引擎), fan 的 clamp 间距算法只此一处实现;line 带 items 时自行按累计宽度推进, 不走假设等宽的 distribute。未知 kind/缺参/target 缺失一律抛错,不兜底。 apply 是唯一触碰 SpriteManager 的三行封装。 测试里 attach 外侧右上角一条按 AlignmentUtils.alignCornerTopRight 的真实实现 (角对角重合,非贴外侧)修正了期望值,未在求解器里做偏移补偿迁就猜测值。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
///////////////////////////////////////////////////////////////
|
||||
////////// 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) {
|
||||
var count = (typeof ctx.count === 'number') ? ctx.count : (n.cols * n.rows);
|
||||
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 为空 = 在目标【内部】对齐;有值 = 与目标对应角重合
|
||||
_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 sprite = { width: n.w, height: n.h };
|
||||
var offset = { x: n.offsetX || 0, y: n.offsetY || 0 };
|
||||
var pos;
|
||||
|
||||
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;
|
||||
default: throw new Error('[EQW_LayoutSolver] 未知 corner: ' + n.corner);
|
||||
}
|
||||
} else {
|
||||
pos = AlignmentUtils.alignTo(target, sprite, n.hAlign, n.vAlign, offset);
|
||||
}
|
||||
|
||||
return [{ x: pos.x, y: pos.y, width: n.w, height: n.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;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user