diff --git a/templates/kin/tree.php b/templates/kin/tree.php index bd64f88..899976a 100644 --- a/templates/kin/tree.php +++ b/templates/kin/tree.php @@ -309,6 +309,39 @@ $payloadJson = json_encode( return uc; } + /** 一"户"的实际横向半宽(用于防重叠;**不是** n*SLOT/2 —— + * 单人是 IW/2、双人是 SLOT —— 用 n*SLOT 会把它算宽 26px)。 */ + function halfW(ui) { + // ⚠️ 一户的**实际**半宽:单人 IW/2 = 44、双人 (2*IW+SP)/2 = 101。 + // 以前写成 (n===2 ? SLOT : IW)/2 = 57 / 44 —— 双人户被算**窄**了 44px, + // 于是防重叠判据偏松、贴着也算"够宽"(实测"堂嫂/舅子"重叠 32px 一直推不开)。 + var n = units[ui].cards.length >= 2 ? 2 : 1; + return (n === 2 ? (2 * IW + SP) : IW) / 2; + } + + /** 同代防重叠:按中心排序,依次保证"前一个右沿 + SP ≤ 后一个左沿"。 + * ⚠️ 统一成这一个函数,三处调用 —— 原来三处各写一遍、边界算法还不一样, + * 于是"儿子/女儿"这种同代相邻的单人户会漏掉(实测重叠 11px)。 */ + function antiOverlapRow(gen) { + var row = []; + units.forEach(function (u, ui) { + if (placed[ui] && placed[ui].gen === gen) { row.push(ui); } + }); + if (row.length < 2) { return; } + row.sort(function (a, b) { return placed[a].uc - placed[b].uc; }); + for (var i = 1; i < row.length; i++) { + var pv = row[i - 1], cu = row[i]; + var need = placed[pv].uc + halfW(pv) + SP + halfW(cu); // 后一个的最小中心 + if (placed[cu].uc < need) { + var d = need - placed[cu].uc; + (function shiftSub(x, dd) { + if (placed[x]) { placed[x].uc += dd; placed[x].x0 += dd; } + (kidsOf[x] || []).forEach(function (k) { if (placed[k]) { shiftSub(k, dd); } }); + })(cu, d); + } + } + } + /* 一代的边界用"已放的户"刷新(对应参考的 measureMaxCoordinate) */ function refreshBounds(gen) { var lo = Infinity, hi = -Infinity, any = false; @@ -317,7 +350,7 @@ $payloadJson = json_encode( if (!p || p.gen !== gen) { return; } any = true; lo = Math.min(lo, p.x0); - hi = Math.max(hi, p.x0 + p.n * SLOT); + hi = Math.max(hi, p.x0 + (p.n === 2 ? 2 * IW + SP : IW)); }); if (!any) { return; } if (!isFinite(genL[gen]) || lo < genL[gen]) { genL[gen] = lo; } @@ -581,22 +614,119 @@ $payloadJson = json_encode( /* ---------- 4. 同代防重叠(保险):按 x 排序后依次推开,子树跟着走 ---------- */ (function antiOverlap() { - for (var g = gmin; g <= gmax; g++) { + for (var g = gmin; g <= gmax; g++) { antiOverlapRow(g); } + })(); + + /* ---------- 4b. 同代重排:让**同一对父母的同胞**挨在一起 ---------- + * + * ⚠️⚠️ 这是"线乱"的真正病根。原来的顺序会让别家的孩子插进同胞之间: + * 实测我这一代是 哥哥(161) 嫂子(275) **堂兄(389) 堂嫂(503)** 老杨(617) … + * —— 堂兄(伯父家的)夹在哥哥和我之间,于是父母连子女的横梁要横跨老远, + * 看着就不像"一家人"。 + * 参考实现是靠"按父辈的游标顺序递归摆放"天然做到这一点的; + * 我们的布局是"先按代平铺、再对齐父母",所以少了这一步,这里补上: + * 按「父户的中心 x」把同代的户重新排序(没有父户的按原相对位置排末尾), + * 然后**横向依次重排**(保持每户宽度与间距),再让父户重新居中。 + */ + (function groupSiblings() { + for (var g2 = gmin; g2 <= gmax; g2++) { var row = []; - units.forEach(function (u, ui) { if (placed[ui] && placed[ui].gen === g) { row.push(ui); } }); - row.sort(function (a, b) { return placed[a].uc - placed[b].uc; }); - for (var i = 1; i < row.length; i++) { - var prev = placed[row[i - 1]], cur = placed[row[i]]; - var prevRight = prev.x0 + prev.n * SLOT; - var curLeft = cur.x0; - if (curLeft < prevRight + 0) { - var d = prevRight - curLeft; - (function shift3(ui, dd) { - if (placed[ui]) { placed[ui].uc += dd; placed[ui].x0 += dd; } - (kidsOf[ui] || []).forEach(function (k) { if (placed[k]) { shift3(k, dd); } }); - })(row[i], d); + units.forEach(function (u, ui) { if (placed[ui] && placed[ui].gen === g2) { row.push(ui); } }); + if (row.length < 2) { continue; } + // 排序键:父户的中心 x(没有父户的用"当前 x",保证相对次序稳定) + function pkey(ui) { + var ps = parentsOf[String(ui)] || []; + var best = null; + ps.forEach(function (p) { + if (placed[p] && (best === null || placed[p].uc < best)) { best = placed[p].uc; } + }); + return best === null ? (placed[ui].uc + 1e6) : best; // 无父户的排到最后 + } + row.sort(function (a, b) { return pkey(a) - pkey(b) || placed[a].uc - placed[b].uc; }); + // 依次重排(保持每户宽度、户间 SP) + var total = 0; + row.forEach(function (ui, i) { + var n = units[ui].cards.length >= 2 ? 2 : 1; + total += (n === 2 ? 2 * IW + SP : IW) + (i ? SP : 0); + }); + var cur = placed[row[0]].x0; // 从原来的最左端起排 + row.forEach(function (ui) { + var n = units[ui].cards.length >= 2 ? 2 : 1; + var w = (n === 2 ? 2 * IW + SP : IW); // ⚠️ 真实宽度,不是 n*SLOT + var d = cur - placed[ui].x0; + if (d) { + (function shift4(x, dd) { + if (placed[x]) { placed[x].uc += dd; placed[x].x0 += dd; } + (kidsOf[x] || []).forEach(function (k) { if (placed[k]) { shift4(k, dd); } }); + })(ui, d); + } + cur += w + SP; + }); + } + // ⚠️ 重排会打乱原有的横向间距 → 必须再跑一轮防重叠(用统一的 antiOverlapRow) + for (var g4 = gmin; g4 <= gmax; g4++) { antiOverlapRow(g4); } + // 再让父户居中于子女之上(防重叠可能又推歪了;一趟就够) + for (var g5 = gmax; g5 >= gmin; g5--) { + Object.keys(placed).forEach(function (kk) { + var ui = +kk; + if (placed[ui].gen !== g5 - 1) { return; } + var ks3 = (kidsOf[ui] || []).filter(function (x) { return placed[x]; }); + if (!ks3.length) { return; } + var xs3 = ks3.map(function (x) { return placed[x].uc; }).sort(function (a, b) { return a - b; }); + var want3 = (xs3[0] + xs3[xs3.length - 1]) / 2; + var d4 = want3 - placed[ui].uc; + if (Math.abs(d4) > 0.5) { placed[ui].uc += d4; placed[ui].x0 += d4; } + }); + } + // 最后一遍防重叠(顺序:重排 → 防重叠 → 父居中 → 防重叠,收敛) + for (var g6 = gmin; g6 <= gmax; g6++) { antiOverlapRow(g6); } + })(); + + /* ---------- 4c. 收尾防重叠(最后一道,必须放在**所有**定位步骤之后) ---------- + * + * ⚠️⚠️ 排序很重要:`groupSiblings()` 会**重排**同代(打乱原有间距), + * 而它若跑在防重叠**之后**,被它挪动过的户就再没人兜底了 —— + * 实测"舅子"(姻亲旁支,由兜底段放在最后)就与"堂嫂"叠了 35px。 + * 所以这里在**所有**定位/重排步骤之后,再统一收一遍。 + */ + (function hardAntiOverlap() { + // ⚠️⚠️ 兜底段(放姻亲旁支,如"舅子")与 groupSiblings() 都会**事后**挪动户, + // 而它们的挪动不走 antiOverlapRow,于是同代仍可能贴上 + // (实测"堂嫂 289-347"与"舅子 315-372"重叠 32px,且多遍 antiOverlapRow 都推不开)。 + // 这里改成**直接按最终坐标硬推**:同代按中心排序,谁跟前一个贴太近就整支右移, + // 循环几遍直到全部满足 —— 不依赖任何前置状态。 + for (var hr = 0; hr < 6; hr++) { + var moved = false; + for (var hg = gmin; hg <= gmax; hg++) { + var hrow = []; + units.forEach(function (u, ui) { + if (placed[ui] && placed[ui].gen === hg) { hrow.push(ui); } + }); + if (hrow.length < 2) { continue; } + hrow.sort(function (a, b) { return placed[a].uc - placed[b].uc; }); + for (var hi = 1; hi < hrow.length; hi++) { + var hp = hrow[hi - 1], hc = hrow[hi]; + var hneed = placed[hp].uc + halfW(hp) + SP + halfW(hc); + if (placed[hc].uc < hneed - 0.5) { + var hd = hneed - placed[hc].uc; + (function hs(x, dd) { + if (placed[x]) { placed[x].uc += dd; placed[x].x0 += dd; } + (kidsOf[x] || []).forEach(function (k) { if (placed[k]) { hs(k, dd); } }); + })(hc, hd); + moved = true; + } } } + if (!moved) { break; } + } + })(); + + (function finalAntiOverlap() { + // ⚠️ 跑**两遍**:单遍不足以收敛 —— 推开一户会连带它的子树一起移, + // 移完可能又与更右的一户贴上(实测"堂嫂 → 舅子"只差 39px, + // 单遍推开后它右边的"老杨"又被挤到,第二遍才稳)。 + for (var round2 = 0; round2 < 3; round2++) { + for (var gf = gmin; gf <= gmax; gf++) { antiOverlapRow(gf); } } })(); @@ -604,7 +734,8 @@ $payloadJson = json_encode( var xs = []; units.forEach(function (u, ui) { var p = placed[ui]; - if (p) { xs.push(p.x0, p.x0 + p.n * SLOT); } + // ⚠️ 户的实际宽度:单人 IW、双人 2*IW+SP —— **不是** n*SLOT + if (p) { xs.push(p.x0, p.x0 + (p.n === 2 ? 2 * IW + SP : IW)); } }); var minX = Math.min.apply(null, xs), maxX = Math.max.apply(null, xs); var W = (maxX - minX) + PAD * 2; @@ -648,50 +779,136 @@ $payloadJson = json_encode( root.insertAdjacentHTML('beforeend', '