/////////////////////////////////////////////////////////////// ////////// 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; } };