v20: 家谱布局推倒重写为区间法(tidy tree)—— 一次成型,无多轮推挤修补
用户反馈:"错的更离谱了,这次连位置都不对了"(v19 实测"我"跑到离父母 800px 的最右侧)。
根因:v18/v19 是「游标铺放 → 防重叠 → 同代重排 → 再防重叠 → 再硬推」
叠了四五层修补,每一层都在破坏上一层的"父居中于子女",多轮互相拉扯后整棵树漂移。
区间法规则(一次成型,天然不叠卡):
① 每棵户子树自底向上算需求宽度 needW = max(自身宽, Σ子女宽 + 间距);
② 自顶向下分配互不重叠的 x 区间,子女依次铺在父区间内;
③ 父户居中于子女的实际跨度。
跨树边(岳父母户也把妻子列为子女)只走「主父户」进树递归
(主位卡是我方血亲的那个父户,服务端给每张卡标 blood),
其余父户只留给画线段 —— 岳父母的子女横梁照样连到妻子,布局不重复铺人。
验证(20 人夹具 + 真实渲染):
· 版面五判据 A-E 全过(8/8):不重叠 / 线在卡后 / 下垂线从夫妻横线中点出 /
同代有空隙 / 横线在两卡缝隙;行分布:兄姐弟妹紧贴我、堂表在伯父下方、
岳父母贴妻子右上、外甥在舅子下方;
· 连线完整性:该连上一代的卡 0 缺线、悬空竖线 0 段;
· 反向验证 3/3:注入坏法(下垂线从左卡出 / needW 砍半 / 中点取左卡)全部变红。
This commit is contained in:
+142
-481
@@ -45,6 +45,24 @@ $rankKey = static function (int $id) use ($people): string {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ---- ④ 打成前端要的数据包(代数 → 户 → 人)----
|
// ---- ④ 打成前端要的数据包(代数 → 户 → 人)----
|
||||||
|
|
||||||
|
// 「我」的血亲集合:只走 parent/child 边(不含配偶)能从「我」到达的人。
|
||||||
|
// 用途:给每张卡标 blood —— 前端据此判定一户的「主父户」
|
||||||
|
// (我的户同时被父母户与岳父母户认作子女,树递归只能走本家那条,
|
||||||
|
// 否则树变图、布局重复铺人;判定标准就是「父户的主位是不是我方血亲」)。
|
||||||
|
$blood = [$meId => true];
|
||||||
|
$bq = [$meId];
|
||||||
|
while ($bq) {
|
||||||
|
$bid = (int)array_shift($bq);
|
||||||
|
foreach (array_merge($graph['parents'][$bid] ?? [], $graph['children'][$bid] ?? []) as $nb) {
|
||||||
|
$nb = (int)$nb;
|
||||||
|
if (!isset($blood[$nb])) {
|
||||||
|
$blood[$nb] = true;
|
||||||
|
$bq[] = $nb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$payloadUnits = [];
|
$payloadUnits = [];
|
||||||
foreach ($units as $i => $u) {
|
foreach ($units as $i => $u) {
|
||||||
$sorted = array_map('intval', $u['ids']);
|
$sorted = array_map('intval', $u['ids']);
|
||||||
@@ -66,6 +84,8 @@ foreach ($units as $i => $u) {
|
|||||||
'me' => $id === (int)$meId,
|
'me' => $id === (int)$meId,
|
||||||
'ph' => (int)($p['is_placeholder'] ?? 0) === 1,
|
'ph' => (int)($p['is_placeholder'] ?? 0) === 1,
|
||||||
'gender' => $g === 'm' ? 'M' : ($g === 'f' ? 'F' : 'U'),
|
'gender' => $g === 'm' ? 'M' : ($g === 'f' ? 'F' : 'U'),
|
||||||
|
// ⚠️ 本家血亲标记:前端用它判定一户的「主父户」(见上方 $blood 的说明)
|
||||||
|
'blood' => isset($blood[$id]),
|
||||||
// ⚠️ 长幼排序键:前端要靠它分「哥哥/弟弟」「姐姐/妹妹」。
|
// ⚠️ 长幼排序键:前端要靠它分「哥哥/弟弟」「姐姐/妹妹」。
|
||||||
// 不用生日硬排(很多人没填生日),用 Kinship 里已有的 rankValueFor ——
|
// 不用生日硬排(很多人没填生日),用 Kinship 里已有的 rankValueFor ——
|
||||||
// 它先看 birth_rank,再退回生日,最后用 id 兜底,保证顺序稳定。
|
// 它先看 birth_rank,再退回生日,最后用 id 兜底,保证顺序稳定。
|
||||||
@@ -82,21 +102,8 @@ foreach ($units as $i => $u) {
|
|||||||
// 我排在她右边"(实测线上就是这个症状:红围脖@639 在我的左边)。
|
// 我排在她右边"(实测线上就是这个症状:红围脖@639 在我的左边)。
|
||||||
//
|
//
|
||||||
// 正解:按「**与"我"的血缘距离**」判定 —— 这一户里离「我」更近的那一位才是主位。
|
// 正解:按「**与"我"的血缘距离**」判定 —— 这一户里离「我」更近的那一位才是主位。
|
||||||
// 我用一次不带配偶边的 BFS 算出血亲集合(Kinship::bloodRelatives 语义),
|
// 血亲集合 $blood 已在循环外算好(只走 parent/child 边的 BFS),
|
||||||
// 两边都在血亲集合里时,退到「与我的步数」比大小。
|
// 两边都在血亲集合里时,退到「与我的步数」比大小。
|
||||||
$blood = [];
|
|
||||||
$bq = [$meId];
|
|
||||||
$blood[$meId] = true;
|
|
||||||
while ($bq) {
|
|
||||||
$bid = array_shift($bq);
|
|
||||||
foreach (array_merge($graph['parents'][$bid] ?? [], $graph['children'][$bid] ?? []) as $nb) {
|
|
||||||
$nb = (int)$nb;
|
|
||||||
if (!isset($blood[$nb])) {
|
|
||||||
$blood[$nb] = true;
|
|
||||||
$bq[] = $nb;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$head = null;
|
$head = null;
|
||||||
$headScore = null;
|
$headScore = null;
|
||||||
foreach ($cards as $c) {
|
foreach ($cards as $c) {
|
||||||
@@ -180,17 +187,17 @@ $payloadJson = json_encode(
|
|||||||
<script src="<?= e(asset('/assets/vendor/family-chart/d3.min.js')) ?>"></script>
|
<script src="<?= e(asset('/assets/vendor/family-chart/d3.min.js')) ?>"></script>
|
||||||
<script>
|
<script>
|
||||||
/*****************************************************************
|
/*****************************************************************
|
||||||
* 家谱布局:仿「亲友+」App 的画法
|
* 家谱布局:区间法(tidy tree)+ 先摆节点、后按节点画线
|
||||||
*
|
*
|
||||||
* 参考实现:gitee.com/coku2333/FamilyTreeView 的 NewFamilyTreeView
|
* 参考过 gitee.com/coku2333/FamilyTreeView(用户 2026-10-02 指定的
|
||||||
* (用户 2026-10-02 指定「按照这个做」)。核心规则:
|
* 「亲友+」效果),但它的游标铺法在我们的"以我为中心、含姻亲旁支"的
|
||||||
* ① 从下往上定位:孙辈 → 子女 → 我 → 父母 → 祖辈
|
* 图上要叠四五层防重叠/重排修补才能不叠卡,每层又互相破坏 —— 最终
|
||||||
* (子层位置定了,父层就定了 —— 父居中于子之上)
|
* 整棵树漂移(用户:"错的更离谱了,这次连位置都不对了")。
|
||||||
* ② 每一代维护 genLeft / genRight 两个游标;往右铺从 genRight 起、
|
* 现在的规则(详见下方第 1 节注释):
|
||||||
* 往左铺从 genLeft 起
|
* ① 每代一行;一对夫妻 = 相邻两张卡;
|
||||||
* ③ 配偶并排在主位右侧(一对夫妻 = 相邻两张卡)
|
* ② 树递归用「区间法」分配互不重叠的 x 区间,父户居中于子女跨度
|
||||||
* ④ 年长的兄弟姐妹往左、年幼的往右;舅姨往右、叔伯姑往左
|
* —— 天然不叠卡,长幼序天然"兄姐在左、弟妹在右";
|
||||||
* ⑤ 连线画在**卡片背后**(可以从卡中心连到卡中心,被卡面挡住)
|
* ③ 连线画在卡片背后,**先把所有节点坐标摆定,再统一按节点画线**。
|
||||||
*****************************************************************/
|
*****************************************************************/
|
||||||
(function () {
|
(function () {
|
||||||
var root = document.getElementById('kinChart');
|
var root = document.getElementById('kinChart');
|
||||||
@@ -271,471 +278,126 @@ $payloadJson = json_encode(
|
|||||||
u._rank = String(u.cards[idx].rank == null ? u.cards[idx].id : u.cards[idx].rank);
|
u._rank = String(u.cards[idx].rank == null ? u.cards[idx].id : u.cards[idx].rank);
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ---------- 1. 游标(每一代的左右边界)---------- */
|
/* ---------- 1. 布局:区间法(tidy tree,一次成型) ----------
|
||||||
var gmin = 0, gmax = 0;
|
|
||||||
units.forEach(function (u) { gmin = Math.min(gmin, u.gen); gmax = Math.max(gmax, u.gen); });
|
|
||||||
var genL = {}, genR = {};
|
|
||||||
function resetCursor(lo, hi) {
|
|
||||||
for (var g = lo; g <= hi; g++) { genL[g] = 0; genR[g] = 0; }
|
|
||||||
}
|
|
||||||
resetCursor(gmin - 1, gmax + 1);
|
|
||||||
|
|
||||||
// 已定位的户:ui -> {gen, uc}(uc = 这一户的中心 x)
|
|
||||||
var placed = {};
|
|
||||||
|
|
||||||
/* 把一户放到某代的"下一个空位",返回它的中心 x
|
|
||||||
toRight=true 从 genR 往右接;false 从 genL 往左接 */
|
|
||||||
function takeSlot(ui, gen, toRight) {
|
|
||||||
var u = units[ui];
|
|
||||||
var n = u.cards.length >= 2 ? 2 : 1;
|
|
||||||
var span = n * SLOT;
|
|
||||||
var x0;
|
|
||||||
if (toRight) {
|
|
||||||
x0 = genR[gen];
|
|
||||||
genR[gen] = x0 + span;
|
|
||||||
} else {
|
|
||||||
genL[gen] -= span;
|
|
||||||
x0 = genL[gen];
|
|
||||||
}
|
|
||||||
// 这一户的中心:两张卡时是两卡中心的中点
|
|
||||||
var uc = x0 + IW / 2 + (n === 2 ? SLOT / 2 : 0);
|
|
||||||
placed[ui] = { gen: gen, uc: uc, n: n, x0: x0 };
|
|
||||||
return uc;
|
|
||||||
}
|
|
||||||
// 把一户**居中**放在 x = uc
|
|
||||||
function putCentered(ui, gen, uc) {
|
|
||||||
var n = units[ui].cards.length >= 2 ? 2 : 1;
|
|
||||||
placed[ui] = { gen: gen, uc: uc, n: n, x0: uc - (n === 2 ? SLOT : IW) / 2 };
|
|
||||||
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;
|
|
||||||
units.forEach(function (u, ui) {
|
|
||||||
var p = placed[ui];
|
|
||||||
if (!p || p.gen !== gen) { return; }
|
|
||||||
any = true;
|
|
||||||
lo = Math.min(lo, p.x0);
|
|
||||||
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; }
|
|
||||||
if (!isFinite(genR[gen]) || hi > genR[gen]) { genR[gen] = hi; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- 2. 核心:放"一户 + 它的子女" ----------
|
|
||||||
childGen = 子女所在的那一代;这一户放在 childGen - 1。
|
|
||||||
完全对应参考的 setPart(position, isRight, ...)。 */
|
|
||||||
function placePart(childGen, toRight, ui) {
|
|
||||||
if (placed[ui]) { return; }
|
|
||||||
var u = units[ui];
|
|
||||||
var kids = (kidsOf[ui] || []).slice();
|
|
||||||
if (!toRight) { kids.reverse(); } // 往左铺时,长幼次序要反过来
|
|
||||||
|
|
||||||
if (!kids.length) {
|
|
||||||
// 没有子女:在"子女代"占位(参考实现就是推进游标留空),
|
|
||||||
// 自己这一户也跟着往前放 —— 这样不会和别人的子女挤在一起
|
|
||||||
var n0 = u.cards.length >= 2 ? 2 : 1;
|
|
||||||
if (toRight) { genR[childGen] += n0 * SLOT; }
|
|
||||||
else { genL[childGen] -= n0 * SLOT; }
|
|
||||||
takeSlot(ui, childGen - 1, toRight);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
kids.forEach(function (k) { placePart(childGen + 1, toRight, k); });
|
|
||||||
|
|
||||||
// 父(这一户)居中于子女之上
|
|
||||||
var first = placed[kids[0]].uc;
|
|
||||||
var last = placed[kids[kids.length - 1]].uc;
|
|
||||||
var mid = (first + last) / 2;
|
|
||||||
putCentered(ui, childGen - 1, mid);
|
|
||||||
refreshBounds(childGen - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 只把"一户"放到某代(它的子女已另行处理)—— 用于兜底与姻亲 */
|
|
||||||
function placeLoose(ui, gen, toRight) {
|
|
||||||
if (placed[ui]) { return; }
|
|
||||||
takeSlot(ui, gen, toRight);
|
|
||||||
refreshBounds(gen);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- 3. 按参考 order 逐代定位 ---------- */
|
|
||||||
(function layoutAll() {
|
|
||||||
/* (1) 我的子女(+1)与孙辈(+2)—— 先把"我"放在原点,再让子女居中于我之下 */
|
|
||||||
putCentered(meUi, 0, 0);
|
|
||||||
genL[0] = -IW / 2; genR[0] = (units[meUi].cards.length >= 2 ? SLOT + IW / 2 : IW / 2);
|
|
||||||
|
|
||||||
var myKids = (kidsOf[meUi] || []).slice();
|
|
||||||
myKids.forEach(function (k) { placePart(1, true, k); });
|
|
||||||
|
|
||||||
/* (2) 我居中于我的子女之上;整支(我 + 子女及以下)一起平移 */
|
|
||||||
if (myKids.length) {
|
|
||||||
var kf = placed[myKids[0]].uc, kl = placed[myKids[myKids.length - 1]].uc;
|
|
||||||
var dx = ((kf + kl) / 2) - placed[meUi].uc;
|
|
||||||
(function shift(ui, d) {
|
|
||||||
if (placed[ui]) { placed[ui].uc += d; placed[ui].x0 += d; }
|
|
||||||
(kidsOf[ui] || []).forEach(function (k) { if (placed[k]) { shift(k, d); } });
|
|
||||||
})(meUi, dx);
|
|
||||||
}
|
|
||||||
resetCursor(gmin - 1, gmax + 1);
|
|
||||||
refreshBounds(0); refreshBounds(1); refreshBounds(2);
|
|
||||||
|
|
||||||
/* (3) 兄弟姐妹:以"父母的子女列表"的长幼序,我在中间 —— 兄姐往左、弟妹往右 */
|
|
||||||
var pu = (parentsOf[meUi] || [])[0];
|
|
||||||
var sibsAll = pu === undefined ? [] : (kidsOf[pu] || []).slice();
|
|
||||||
var myPos = sibsAll.indexOf(meUi);
|
|
||||||
var older = myPos > 0 ? sibsAll.slice(0, myPos) : []; // 长幼序里排在我前面的 = 兄姐
|
|
||||||
var younger = myPos >= 0 ? sibsAll.slice(myPos + 1) : []; // 排在我后面的 = 弟妹
|
|
||||||
|
|
||||||
// ⚠️⚠️ 兄姐/弟妹必须**紧贴着我**(在上一代"父母的")。参考实现里它们是
|
|
||||||
// "从 genLeft/genRight 往外铺",但那是**接着父母的子女块**往外长 ——
|
|
||||||
// 所以才有"哥哥在我左边、弟弟在我右边"的观感。
|
|
||||||
// 我这一版第一稿直接调 placePart() 从游标末尾起,结果兄姐/弟妹被推到
|
|
||||||
// 同代的最外侧(中间还夹着堂表亲),看着就像"外人"(实测截图确认)。
|
|
||||||
// 正解:以"我的中心"为基准,向左/向右**依次紧贴**摆放,
|
|
||||||
// 每放一户就把基准推进一户宽(并带上它的子女)。
|
|
||||||
var slotW = IW + SP;
|
|
||||||
// 兄姐:离我最近的先放(大哥最左 → 依次向左)
|
|
||||||
var cur = placed[meUi].x0; // 我这一户的左边缘
|
|
||||||
older.forEach(function (s) { // older 已是"长→幼",从最近(幼)开始向左
|
|
||||||
var n = units[s].cards.length >= 2 ? 2 : 1;
|
|
||||||
var w = n * slotW;
|
|
||||||
var uc = cur - SP - w / 2;
|
|
||||||
putCentered(s, 0, uc);
|
|
||||||
// 它的子女(侄甥)放它下方
|
|
||||||
(kidsOf[s] || []).forEach(function (k) { placePart(1, true, k); });
|
|
||||||
cur = uc - w / 2;
|
|
||||||
});
|
|
||||||
// 弟妹:往右紧贴
|
|
||||||
var curR = placed[meUi].x0 + (units[meUi].cards.length >= 2 ? 2 * slotW : IW);
|
|
||||||
younger.forEach(function (s) {
|
|
||||||
var n2 = units[s].cards.length >= 2 ? 2 : 1;
|
|
||||||
var w2 = n2 * slotW;
|
|
||||||
var uc2 = curR + SP + w2 / 2;
|
|
||||||
putCentered(s, 0, uc2);
|
|
||||||
(kidsOf[s] || []).forEach(function (k) { placePart(1, true, k); });
|
|
||||||
curR = uc2 + w2 / 2;
|
|
||||||
});
|
|
||||||
// 兄姐是"长幼序里排在我前面的",最年长的应在最左 → 反向再摆一次
|
|
||||||
// (上面是从最近往左排,最年长的自然落在最左,正是要的效果)
|
|
||||||
|
|
||||||
/* (4) 父母(-1)居中于我之上;祖辈(-2)居中于父母之上 */
|
|
||||||
if (pu !== undefined && !placed[pu]) {
|
|
||||||
putCentered(pu, -1, placed[meUi].uc);
|
|
||||||
refreshBounds(-1);
|
|
||||||
var gps = (parentsOf[pu] || []).slice();
|
|
||||||
gps.forEach(function (gp, i) {
|
|
||||||
if (placed[gp]) { return; }
|
|
||||||
putCentered(gp, -2, placed[pu].uc + (gps.length > 1 ? (i === 0 ? -SLOT / 2 : SLOT / 2) : 0));
|
|
||||||
refreshBounds(-2);
|
|
||||||
});
|
|
||||||
/* (5) 父母的兄弟姐妹:舅姨(母系)往右、叔伯姑(父系)往左,他们的子女是堂表亲(0 代) */
|
|
||||||
// 判断哪个祖辈是父系:父 = 父母这一户里"性别为男"的那张卡(取不到就用主位)
|
|
||||||
var pr = units[pu];
|
|
||||||
var fatherId = null;
|
|
||||||
pr.cards.forEach(function (c) { if (c.gender === 'M') { fatherId = String(c.id); } });
|
|
||||||
if (!fatherId) { fatherId = pr._head; }
|
|
||||||
// ⚠️ 叔伯姑/舅姨要**紧挨着父母**摆:父系往父亲那侧(左)、母系往母亲那侧(右)。
|
|
||||||
// 第一稿直接 placePart(0, 左/右) —— 那是从"整代游标"的末尾起,
|
|
||||||
// 叔伯被推到很远的外侧,中间空一大段(实测截图里"伯父"跑到爸爸右边很远)。
|
|
||||||
// 正解:以"父母这一户"的两端为基准向外铺。
|
|
||||||
var puW = units[pu].cards.length >= 2 ? 2 * slotW : IW;
|
|
||||||
var puLeft = placed[pu].uc - puW / 2;
|
|
||||||
var puRight = placed[pu].uc + puW / 2;
|
|
||||||
var bySide = { left: [], right: [] };
|
|
||||||
// ⚠️⚠️ 判定「这一位是我父系的还是母系的」要看**他自己的父母**里
|
|
||||||
// 有没有那位父系祖辈 —— 而不是"去祖辈户里找爸爸"(那是恒假的写法,
|
|
||||||
// 第一稿就这么写,结果所有叔伯都被当成母系、全跑到右边)。
|
|
||||||
// fatherId = 我爸的 id;父系祖辈 = 我爸的父母里那一位。
|
|
||||||
var fatherSideGps = (personParents[String(fatherId)] || []).slice();
|
|
||||||
gps.forEach(function (gp) {
|
|
||||||
(kidsOf[gp] || []).forEach(function (s) {
|
|
||||||
if (s === pu || placed[s]) { return; }
|
|
||||||
// s 的父母里有没有"父系祖辈所在的户"
|
|
||||||
var ps2 = personParents[units[s]._head] || [];
|
|
||||||
var isFatherSide = ps2.indexOf(gp) >= 0 && fatherSideGps.indexOf(gp) >= 0;
|
|
||||||
bySide[isFatherSide ? 'left' : 'right'].push(s);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
// 叔伯姑/舅姨的子女(堂表亲):**紧贴其父母下方**放。
|
|
||||||
// ⚠️ 不能调 placePart() —— 那会从"整代游标末尾"起、还会递归整支,
|
|
||||||
// 结果堂兄跑到我的兄姐位置上、两者重叠(实测抓到 1 处重叠)。
|
|
||||||
// 这里按"父户中心"居中摆,若与已有的撞上再顺延。
|
|
||||||
function placeCousins(s, gen) {
|
|
||||||
var ks = (kidsOf[s] || []).slice();
|
|
||||||
if (!ks.length) { return; }
|
|
||||||
var span = ks.length * slotW;
|
|
||||||
var startC = placed[s].uc - (span - slotW) / 2;
|
|
||||||
ks.forEach(function (k, i) {
|
|
||||||
if (placed[k]) { return; }
|
|
||||||
var n = units[k].cards.length >= 2 ? 2 : 1;
|
|
||||||
var w = n * slotW;
|
|
||||||
putCentered(k, gen, startC + i * slotW + (slotW - w) / 2 + w / 2);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// 父系:从父亲的左边往外铺(长幼序倒过来 —— 长叔伯更靠外)
|
|
||||||
var lcur = puLeft;
|
|
||||||
bySide.left.slice().reverse().forEach(function (s) {
|
|
||||||
var n = units[s].cards.length >= 2 ? 2 : 1;
|
|
||||||
var w = n * slotW;
|
|
||||||
var uc = lcur - SP - w / 2;
|
|
||||||
putCentered(s, -1, uc);
|
|
||||||
placeCousins(s, 0);
|
|
||||||
lcur = uc - w / 2;
|
|
||||||
});
|
|
||||||
// 母系:从母亲的右边往外铺
|
|
||||||
var rcur = puRight;
|
|
||||||
bySide.right.forEach(function (s) {
|
|
||||||
var n2 = units[s].cards.length >= 2 ? 2 : 1;
|
|
||||||
var w2 = n2 * slotW;
|
|
||||||
var uc2 = rcur + SP + w2 / 2;
|
|
||||||
putCentered(s, -1, uc2);
|
|
||||||
placeCousins(s, 0);
|
|
||||||
rcur = uc2 + w2 / 2;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* (6) 兜底:还没定位的(姻亲岳父母、其它旁支)按"离我这支最近"贴到同代右侧 */
|
|
||||||
var rest = [];
|
|
||||||
units.forEach(function (u, ui) { if (!placed[ui]) { rest.push(ui); } });
|
|
||||||
// 先放"与我这一支有直接关系"的(比如配偶的父母),再放更远的
|
|
||||||
rest.sort(function (a, b) {
|
|
||||||
var da = nearDist(a), db = nearDist(b);
|
|
||||||
return (units[a].gen - units[b].gen) || (da - db) || (a - b);
|
|
||||||
});
|
|
||||||
rest.forEach(function (ui) {
|
|
||||||
var g = units[ui].gen;
|
|
||||||
// 尽量靠到"它的子女"旁边(姻亲的父母 → 贴在他们女儿(我配偶)那一支的右侧)
|
|
||||||
// ⚠️ 姻亲(岳父母):他们的位置应该**贴在他们女儿(我配偶)那一支的上方**,
|
|
||||||
// 而不是"飘到同代最右"。第一稿就是这么写的 —— 结果岳父母跑到爸爸右边、
|
|
||||||
// 离我妻子很远,看着像别人家的人(实测截图确认)。
|
|
||||||
// 正解:锚到"它的子女"(= 我配偶所在的那一户)的上方居中。
|
|
||||||
var ks = kidsOf[ui] || [];
|
|
||||||
var anchor = null;
|
|
||||||
ks.forEach(function (k) { if (placed[k] !== undefined && anchor === null) { anchor = placed[k]; } });
|
|
||||||
if (anchor !== undefined && anchor !== null && anchor.gen === g + 1) {
|
|
||||||
// 与"同代已有的"错开:若锚点上方已被别人占,就往右让
|
|
||||||
var want = anchor.uc;
|
|
||||||
var n = units[ui].cards.length >= 2 ? 2 : 1;
|
|
||||||
var w = n * slotW;
|
|
||||||
// 与同代已有的户依次避让(只往右让,避免来回挪)
|
|
||||||
var row = [];
|
|
||||||
units.forEach(function (u2, ui2) {
|
|
||||||
if (placed[ui2] && placed[ui2].gen === g && ui2 !== ui) { row.push(ui2); }
|
|
||||||
});
|
|
||||||
row.sort(function (a, b) { return placed[a].uc - placed[b].uc; });
|
|
||||||
var half = w / 2;
|
|
||||||
for (var ri = 0; ri < row.length; ri++) {
|
|
||||||
var rp = placed[row[ri]];
|
|
||||||
var rn = units[row[ri]].cards.length >= 2 ? 2 : 1;
|
|
||||||
var rHalf = rn * slotW / 2;
|
|
||||||
if (Math.abs(want - rp.uc) < (half + rHalf + SP / 2)) {
|
|
||||||
want = rp.uc + rHalf + SP / 2 + half;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
putCentered(ui, g, want);
|
|
||||||
} else {
|
|
||||||
// ⚠️ 没有子女、也不在主干上的旁支(比如"舅子"= 我妻子的兄弟):
|
|
||||||
// 要**贴到它父母的户旁边**,而不是"推到整代最右"。
|
|
||||||
// 第一稿用 placeLoose(往右) —— 结果舅子跑到离我很远的最右侧,
|
|
||||||
// 还和同代的人撞上(实测 D 项最小间隙 -36px)。
|
|
||||||
var ps3 = parentsOf[String(ui)] || [];
|
|
||||||
var panchor = null;
|
|
||||||
ps3.forEach(function (p3) { if (placed[p3] && panchor === null) { panchor = p3; } });
|
|
||||||
if (panchor !== null) {
|
|
||||||
// 放在父母那一户的右侧紧邻处(父母的其它子女已经在旁边了)
|
|
||||||
var pn = units[panchor].cards.length >= 2 ? 2 : 1;
|
|
||||||
var pw = pn * slotW;
|
|
||||||
var want2 = placed[panchor].uc + pw / 2 + SP + slotW / 2;
|
|
||||||
// 与同代已有的人避让
|
|
||||||
var row2 = [];
|
|
||||||
units.forEach(function (u3, ui3) {
|
|
||||||
if (placed[ui3] && placed[ui3].gen === g && ui3 !== ui) { row2.push(ui3); }
|
|
||||||
});
|
|
||||||
row2.sort(function (a, b) { return placed[a].uc - placed[b].uc; });
|
|
||||||
for (var rj = 0; rj < row2.length; rj++) {
|
|
||||||
var rp2 = placed[row2[rj]];
|
|
||||||
var rn2 = units[row2[rj]].cards.length >= 2 ? 2 : 1;
|
|
||||||
if (Math.abs(want2 - rp2.uc) < (slotW / 2 + rn2 * slotW / 2)) {
|
|
||||||
want2 = rp2.uc + rn2 * slotW / 2 + SP + slotW / 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
putCentered(ui, g, want2);
|
|
||||||
} else {
|
|
||||||
placeLoose(ui, g, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function nearDist(ui) {
|
|
||||||
var ps = parentsOf[ui] || [];
|
|
||||||
var best = 9;
|
|
||||||
ps.forEach(function (p) {
|
|
||||||
// 到「我」这一支的代数距离
|
|
||||||
best = Math.min(best, Math.abs(units[p].gen - 0) + 1);
|
|
||||||
});
|
|
||||||
return best;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
/* ---------- 4. 同代防重叠(保险):按 x 排序后依次推开,子树跟着走 ---------- */
|
|
||||||
(function antiOverlap() {
|
|
||||||
for (var g = gmin; g <= gmax; g++) { antiOverlapRow(g); }
|
|
||||||
})();
|
|
||||||
|
|
||||||
/* ---------- 4b. 同代重排:让**同一对父母的同胞**挨在一起 ----------
|
|
||||||
*
|
*
|
||||||
* ⚠️⚠️ 这是"线乱"的真正病根。原来的顺序会让别家的孩子插进同胞之间:
|
* 为什么整个推倒重写(用户 2026-10-02:"错的更离谱了,这次连位置都不对了"):
|
||||||
* 实测我这一代是 哥哥(161) 嫂子(275) **堂兄(389) 堂嫂(503)** 老杨(617) …
|
* 之前的版本是「游标铺放 → 防重叠 → 同代重排 → 再防重叠 → 再硬推」
|
||||||
* —— 堂兄(伯父家的)夹在哥哥和我之间,于是父母连子女的横梁要横跨老远,
|
* 叠了四五层修补,每一层都在破坏上一层的"父居中于子女",
|
||||||
* 看着就不像"一家人"。
|
* 多轮互相拉扯后整棵树漂移(实测"我"跑到离父母 800px 的最右侧)。
|
||||||
* 参考实现是靠"按父辈的游标顺序递归摆放"天然做到这一点的;
|
|
||||||
* 我们的布局是"先按代平铺、再对齐父母",所以少了这一步,这里补上:
|
|
||||||
* 按「父户的中心 x」把同代的户重新排序(没有父户的按原相对位置排末尾),
|
|
||||||
* 然后**横向依次重排**(保持每户宽度与间距),再让父户重新居中。
|
|
||||||
*/
|
|
||||||
(function groupSiblings() {
|
|
||||||
for (var g2 = gmin; g2 <= gmax; g2++) {
|
|
||||||
var row = [];
|
|
||||||
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。
|
* needW = max( 自身宽, Σ子女needW + SP×(子女数−1) )
|
||||||
* 所以这里在**所有**定位/重排步骤之后,再统一收一遍。
|
* ② 自顶向下分配**互不重叠的 x 区间**:子女依次铺在父区间内;
|
||||||
|
* ③ 父户居中于子女的实际跨度。
|
||||||
|
* 区间天然不重叠 ⇒ 任何两张卡绝不重叠,**不需要任何后续推挤修补**。
|
||||||
|
*
|
||||||
|
* 跨树边(岳父母户也把妻子列为子女):每户只认**主父户**进树递归
|
||||||
|
* (主位卡是"我方血亲"的那个父户,服务端已给每张卡标了 blood),
|
||||||
|
* 其余父户只在**画线**时用 —— 岳父母的子女横梁照样连到妻子,
|
||||||
|
* 但布局上岳父母树不会把"我的户"再铺一遍。
|
||||||
*/
|
*/
|
||||||
(function hardAntiOverlap() {
|
var rawKids = kidsOf; // 原始子女关系(画线用;跨树边保留)
|
||||||
// ⚠️⚠️ 兜底段(放姻亲旁支,如"舅子")与 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() {
|
var mainParent = {}; // ui -> 主父户 ui
|
||||||
// ⚠️ 跑**两遍**:单遍不足以收敛 —— 推开一户会连带它的子树一起移,
|
var kidList = {}; // ui -> [子女户 ui,...](只含"把我当主父户"的)
|
||||||
// 移完可能又与更右的一户贴上(实测"堂嫂 → 舅子"只差 39px,
|
units.forEach(function (u, ui) { u._blood = !!(u.cards[0] && u.cards[0].blood); });
|
||||||
// 单遍推开后它右边的"老杨"又被挤到,第二遍才稳)。
|
units.forEach(function (u, ki) {
|
||||||
for (var round2 = 0; round2 < 3; round2++) {
|
var ps = parentsOf[ki] || [];
|
||||||
for (var gf = gmin; gf <= gmax; gf++) { antiOverlapRow(gf); }
|
if (!ps.length) { return; }
|
||||||
|
var main = null;
|
||||||
|
ps.forEach(function (p) { if (main === null && units[p]._blood) { main = p; } });
|
||||||
|
if (main === null) { main = ps[0]; }
|
||||||
|
mainParent[ki] = main;
|
||||||
|
// kidList 沿 kidsOf 的遍历顺序 push —— 服务端的长幼序原样保留
|
||||||
|
(kidList[main] = kidList[main] || []).push(ki);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 一户的实际宽度:单人 IW、夫妻两张 2*IW+SP(不是 n*SLOT) */
|
||||||
|
function ownW(ui) {
|
||||||
|
return units[ui].cards.length >= 2 ? 2 * IW + SP : IW;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ① 需求宽度:自底向上 */
|
||||||
|
var needW = {};
|
||||||
|
function calcNeed(ui) {
|
||||||
|
if (needW[ui] !== undefined) { return needW[ui]; }
|
||||||
|
var total = 0;
|
||||||
|
(kidList[ui] || []).forEach(function (k, i) {
|
||||||
|
total += calcNeed(k) + (i ? SP : 0);
|
||||||
|
});
|
||||||
|
needW[ui] = Math.max(ownW(ui), total);
|
||||||
|
return needW[ui];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ②③ 分配区间:自顶向下;父户居中于子女的实际跨度 */
|
||||||
|
var placed = {}; // ui -> {gen, x0}
|
||||||
|
function assign(ui, x0) {
|
||||||
|
var own = ownW(ui);
|
||||||
|
var ks = kidList[ui] || [];
|
||||||
|
var total = 0;
|
||||||
|
ks.forEach(function (k, i) { total += needW[k] + (i ? SP : 0); });
|
||||||
|
var kx = x0 + (needW[ui] - total) / 2; // 子女在父区间内居中起铺
|
||||||
|
ks.forEach(function (k) { assign(k, kx); kx += needW[k] + SP; });
|
||||||
|
// 居中锚:所有子女(**含跨树子女**——他们已在别的树里放好)的实际跨度中点
|
||||||
|
var lo = null, hi = null;
|
||||||
|
(rawKids[ui] || []).forEach(function (k) {
|
||||||
|
if (!placed[k]) { return; }
|
||||||
|
var c = placed[k].x0 + ownW(k) / 2;
|
||||||
|
if (lo === null || c < lo) { lo = c; }
|
||||||
|
if (hi === null || c > hi) { hi = c; }
|
||||||
|
});
|
||||||
|
var want = (lo === null) ? x0 + (needW[ui] - own) / 2 : (lo + hi) / 2 - own / 2;
|
||||||
|
// 父户不得越出自己被分配的区间(越出会撞上左邻/右邻子树)
|
||||||
|
placed[ui] = {
|
||||||
|
gen: units[ui].gen,
|
||||||
|
x0: Math.max(x0, Math.min(want, x0 + needW[ui] - own))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 森林:无主父户的户是树根;含「我」的树先放,其余树(姻亲)依次排在右侧 */
|
||||||
|
function treeHasMe(root) {
|
||||||
|
var st = [root], seen = {};
|
||||||
|
while (st.length) {
|
||||||
|
var x = st.pop();
|
||||||
|
if (seen[x]) { continue; }
|
||||||
|
seen[x] = true;
|
||||||
|
if (x === meUi) { return true; }
|
||||||
|
(kidList[x] || []).forEach(function (k) { st.push(k); });
|
||||||
}
|
}
|
||||||
})();
|
return false;
|
||||||
|
}
|
||||||
|
var roots = [];
|
||||||
|
units.forEach(function (u, ui) { if (mainParent[ui] === undefined) { roots.push(ui); } });
|
||||||
|
roots.sort(function (a, b) {
|
||||||
|
var ma = treeHasMe(a), mb = treeHasMe(b);
|
||||||
|
return (ma === mb) ? ((units[a].gen - units[b].gen) || (a - b)) : (ma ? -1 : 1);
|
||||||
|
});
|
||||||
|
function subtreeRight(root) {
|
||||||
|
var hi = -Infinity;
|
||||||
|
(function walk(x) {
|
||||||
|
var p = placed[x];
|
||||||
|
if (p) { hi = Math.max(hi, p.x0 + ownW(x)); }
|
||||||
|
(kidList[x] || []).forEach(walk);
|
||||||
|
})(root);
|
||||||
|
return hi;
|
||||||
|
}
|
||||||
|
var cursorX = 0;
|
||||||
|
var TREE_GAP = SP; // 树与树之间的留白(姻亲树贴着主树放,
|
||||||
|
// 让岳父母尽量靠近妻子;区间法保证不重叠)
|
||||||
|
roots.forEach(function (r) {
|
||||||
|
calcNeed(r);
|
||||||
|
assign(r, cursorX);
|
||||||
|
cursorX = subtreeRight(r) + TREE_GAP;
|
||||||
|
});
|
||||||
|
|
||||||
/* ---------- 5. 落最终坐标 ---------- */
|
/* ---------- 5. 落最终坐标 ---------- */
|
||||||
|
var gmin = 0, gmax = 0;
|
||||||
|
units.forEach(function (u) { gmin = Math.min(gmin, u.gen); gmax = Math.max(gmax, u.gen); });
|
||||||
var xs = [];
|
var xs = [];
|
||||||
units.forEach(function (u, ui) {
|
units.forEach(function (u, ui) {
|
||||||
var p = placed[ui];
|
var p = placed[ui];
|
||||||
// ⚠️ 户的实际宽度:单人 IW、双人 2*IW+SP —— **不是** n*SLOT
|
if (p) { xs.push(p.x0, p.x0 + ownW(ui)); }
|
||||||
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 minX = Math.min.apply(null, xs), maxX = Math.max.apply(null, xs);
|
||||||
var W = (maxX - minX) + PAD * 2;
|
var W = (maxX - minX) + PAD * 2;
|
||||||
@@ -876,8 +538,7 @@ $payloadJson = json_encode(
|
|||||||
// 横梁的 y 落在**两代之间的空隙**里,那里**没有任何卡片**,所以横梁不穿卡。
|
// 横梁的 y 落在**两代之间的空隙**里,那里**没有任何卡片**,所以横梁不穿卡。
|
||||||
// 真正的问题是**布局**:同一对父母的子女被别家的孩子插在中间
|
// 真正的问题是**布局**:同一对父母的子女被别家的孩子插在中间
|
||||||
// (实测"堂兄"夹在"哥哥"和"我"之间),横梁才被迫跨得很远。
|
// (实测"堂兄"夹在"哥哥"和"我"之间),横梁才被迫跨得很远。
|
||||||
// 那个已经在下面的 `groupSiblings()` 里修掉了(同父的子女会被排到一起)。
|
// 区间法布局里同一父户的子女在它的区间内相邻铺开,横梁自然是短的。
|
||||||
// 所以这里恢复成"最左 → 最右"的整条横梁,看起来才是一家人。
|
|
||||||
var ks = kids.map(function (kk) { return anchor[kk].mid; })
|
var ks = kids.map(function (kk) { return anchor[kk].mid; })
|
||||||
.sort(function (x, y2) { return x - y2; });
|
.sort(function (x, y2) { return x - y2; });
|
||||||
if (ks.length > 1) { line(ks[0], busY, ks[ks.length - 1], busY); }
|
if (ks.length > 1) { line(ks[0], busY, ks[ks.length - 1], busY); }
|
||||||
|
|||||||
Reference in New Issue
Block a user