Files
erqiwang_youle/client/js/01_SubGame/codes/ui/LayoutSolver.js
T
joywayerandClaude Opus 5 2d0469e7f3 二七王:前端 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>
2026-08-26 20:09:26 +08:00

197 lines
8.3 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) {
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;
}
};