v19: 家谱画线重写 —— 先定节点坐标、再按节点画线(用户建议的思路)
用户反馈(原话):「还是有问题,主要是线的问题,很多线画的不到位,要么是连的不到位,
要么就是和下一代的节点没连上。你是用 convas 画的吗?应该把每个节点的位置都摆好之后,
再根据节点进行画线」。
我按这个思路把画线整段重写了,并用脚本把「哪张卡没连上」**量成数字**(新工具
`_tooling/_fb_kin_linecheck.py`)。量出来的真问题与修法:
1. **两套坐标混着用**(这是"连不到位"的根因):
横梁端点用了 `placed[k].uc`(**户中心**),而卡片真实中心是 `pos[id].x`;
「子女接父母」的竖线起点取"卡顶",横梁 y 却按"父户那一代的 y"推 —— 不在同一坐标系。
→ 现在统一:**先落定所有 pos[],再由 pos 反推每户的画线锚点**(left/right/mid/top/bottom),
画线只引用这些锚点(新 `anchor[]`,模板里显式标注了这条契约)。
2. **横梁 y 用错了代**:原用 `nextGenY(我的代数)` —— 但**叔伯的子女与我同代**(不是下一代),
于是横梁画到别处、孩子接不上。
→ 改成由**孩子自己的卡顶**算:`busY = min(孩子卡顶) - SP/2`。
3. **「接入父母横梁」用错了锚**:原是"本代卡顶 - SP/2"(只在相邻两代才成立),
姻亲旁支一错位就接不上(实测 6 张卡"有父母却没有线连上去")。
→ 改成查**父母那一户的 `busY`**。
4. **同胞被别家孩子插在中间**("线乱"的真正病根):
实测我这一代原为 哥哥(161) 嫂子(275) **堂兄(389) 堂嫂(503)** 老杨(617) ——
堂兄夹在哥哥和我之间,父母连子女的横梁被迫横跨,看着不像一家人。
→ 新增 `groupSiblings()`:同代按「父户的中心 x」重排,让同胞挨在一起;
重排后再跑防重叠 + 父户居中(收敛)。
5. **户宽算错 26px**(防重叠一直推不开的原因):
边界用 `n * SLOT`,而单人户实际是 `IW`、双人户是 `2*IW+SP` —— 处处偏大。
→ 全部改成真实宽度(新增 `halfW()`),并加 `hardAntiOverlap()` 按最终坐标硬推、循环收敛。
**战绩**(`_fb_kin_linecheck.py` 是这轮新写的诊断工具,它自己也被修了三次判据 ——
"端点恰好等于卡边"→"区间覆盖"→"半个槽位",前两版都误报):
· 20 人夹具:「应连上一代却无线」= **无**;「接不上的竖线」= **0 段**;重叠 0 对
· 版面质量 8 项全过(含 A 不重叠 / D 有空隙 / C 下垂线从中点出)
· **真实线上数据(11 人)**:重叠 0 对、19 段连线、无 JS 报错
· e2e 553 条全过
⚠️ 顺带发现的数据问题(**不是代码问题**,已告知用户):
真实数据里 **爸爸与妈妈之间没有登记夫妻关系**(妈妈是单独一户),
所以图上他们是并排但不算一对;补上「配偶」后会自动合并成一户。
This commit is contained in:
+267
-50
@@ -309,6 +309,39 @@ $payloadJson = json_encode(
|
|||||||
return uc;
|
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) */
|
/* 一代的边界用"已放的户"刷新(对应参考的 measureMaxCoordinate) */
|
||||||
function refreshBounds(gen) {
|
function refreshBounds(gen) {
|
||||||
var lo = Infinity, hi = -Infinity, any = false;
|
var lo = Infinity, hi = -Infinity, any = false;
|
||||||
@@ -317,7 +350,7 @@ $payloadJson = json_encode(
|
|||||||
if (!p || p.gen !== gen) { return; }
|
if (!p || p.gen !== gen) { return; }
|
||||||
any = true;
|
any = true;
|
||||||
lo = Math.min(lo, p.x0);
|
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 (!any) { return; }
|
||||||
if (!isFinite(genL[gen]) || lo < genL[gen]) { genL[gen] = lo; }
|
if (!isFinite(genL[gen]) || lo < genL[gen]) { genL[gen] = lo; }
|
||||||
@@ -581,22 +614,119 @@ $payloadJson = json_encode(
|
|||||||
|
|
||||||
/* ---------- 4. 同代防重叠(保险):按 x 排序后依次推开,子树跟着走 ---------- */
|
/* ---------- 4. 同代防重叠(保险):按 x 排序后依次推开,子树跟着走 ---------- */
|
||||||
(function antiOverlap() {
|
(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 = [];
|
var row = [];
|
||||||
units.forEach(function (u, ui) { if (placed[ui] && placed[ui].gen === g) { row.push(ui); } });
|
units.forEach(function (u, ui) { if (placed[ui] && placed[ui].gen === g2) { row.push(ui); } });
|
||||||
row.sort(function (a, b) { return placed[a].uc - placed[b].uc; });
|
if (row.length < 2) { continue; }
|
||||||
for (var i = 1; i < row.length; i++) {
|
// 排序键:父户的中心 x(没有父户的用"当前 x",保证相对次序稳定)
|
||||||
var prev = placed[row[i - 1]], cur = placed[row[i]];
|
function pkey(ui) {
|
||||||
var prevRight = prev.x0 + prev.n * SLOT;
|
var ps = parentsOf[String(ui)] || [];
|
||||||
var curLeft = cur.x0;
|
var best = null;
|
||||||
if (curLeft < prevRight + 0) {
|
ps.forEach(function (p) {
|
||||||
var d = prevRight - curLeft;
|
if (placed[p] && (best === null || placed[p].uc < best)) { best = placed[p].uc; }
|
||||||
(function shift3(ui, dd) {
|
});
|
||||||
if (placed[ui]) { placed[ui].uc += dd; placed[ui].x0 += dd; }
|
return best === null ? (placed[ui].uc + 1e6) : best; // 无父户的排到最后
|
||||||
(kidsOf[ui] || []).forEach(function (k) { if (placed[k]) { shift3(k, dd); } });
|
}
|
||||||
})(row[i], d);
|
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 = [];
|
var xs = [];
|
||||||
units.forEach(function (u, ui) {
|
units.forEach(function (u, ui) {
|
||||||
var p = placed[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 minX = Math.min.apply(null, xs), maxX = Math.max.apply(null, xs);
|
||||||
var W = (maxX - minX) + PAD * 2;
|
var W = (maxX - minX) + PAD * 2;
|
||||||
@@ -648,50 +779,136 @@ $payloadJson = json_encode(
|
|||||||
root.insertAdjacentHTML('beforeend',
|
root.insertAdjacentHTML('beforeend',
|
||||||
'<div class="kin-stage" style="width:' + W + 'px;height:' + H + 'px">' + html + '</div>');
|
'<div class="kin-stage" style="width:' + W + 'px;height:' + H + 'px">' + html + '</div>');
|
||||||
|
|
||||||
/* ---------- 7. 连线(画在卡片背后,所以可以从卡中心连到卡中心)---------- */
|
/* ---------- 7. 连线 ----------
|
||||||
|
*
|
||||||
|
* ⚠️⚠️ v19:**先把所有节点坐标摆定,再统一按节点坐标画线**(用户 2026-10-02 的建议原话:
|
||||||
|
* 「应该把每个节点的位置都摆好之后,再根据节点进行画线」)。
|
||||||
|
* 上一版是"边摆放边画线",于是两套坐标混着用:
|
||||||
|
* · 横梁端点用了 `placed[k].uc`(**户中心**),而卡片的真实中心是 `pos[id].x` ——
|
||||||
|
* 户中心与卡中心只在"单人户"下相等,双人户差半个槽位;
|
||||||
|
* · 「子女接父母」的竖线起点取"卡顶",而横梁的 y 是按"父户那一代的 y"推的,
|
||||||
|
* 两者不在同一个坐标系里。
|
||||||
|
* 结果:实测 5 张卡「有父母却没有线连上去」、17 段线两端都不碰任何卡片(悬空)。
|
||||||
|
*
|
||||||
|
* 现在统一成三步:
|
||||||
|
* ① 每个"人"的坐标已经在 pos[] 里定死(那是唯一的真值);
|
||||||
|
* ② 每"户"从它的卡片坐标推出:左卡中心、右卡中心、横线中点、卡顶/卡底 y;
|
||||||
|
* ③ 画线只用这些**由节点坐标推出的值**,不再引用放置阶段的游标数据。
|
||||||
|
*
|
||||||
|
* 线的三段(与参考实现一致):
|
||||||
|
* A. 夫妻横线:两张卡中心之间(y = 卡中心高度)
|
||||||
|
* B. 下垂线:从**横线中点**往下 → 到"子女代的横梁"(y = 下一代卡顶 - SP/2)
|
||||||
|
* C. 横梁 + 分叉:横梁连到每个孩子的**卡顶中点**,再各自往下一点点接上卡
|
||||||
|
* 另外每人若有父母,从卡顶往上画一小段接到横梁(保证"和下一代连上"是闭合的)。
|
||||||
|
*/
|
||||||
var paths = [], bars = [], drops = [];
|
var paths = [], bars = [], drops = [];
|
||||||
|
|
||||||
function line(x1, y1, x2, y2) {
|
function line(x1, y1, x2, y2) {
|
||||||
|
if (Math.abs(x1 - x2) < 0.05 && Math.abs(y1 - y2) < 0.05) { return; }
|
||||||
paths.push('M' + x1.toFixed(1) + ',' + y1.toFixed(1) + 'L' + x2.toFixed(1) + ',' + y2.toFixed(1));
|
paths.push('M' + x1.toFixed(1) + ',' + y1.toFixed(1) + 'L' + x2.toFixed(1) + ',' + y2.toFixed(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- ② 由「人」的坐标推出「户」的画线锚点 ----
|
||||||
|
// anchor[ui] = {gen, y, left, right, mid, top, bottom, hasBar, cardIds}
|
||||||
|
var anchor = {};
|
||||||
units.forEach(function (u, ui) {
|
units.forEach(function (u, ui) {
|
||||||
var p = placed[ui];
|
var ids = u.cards.map(function (c) { return String(c.id); })
|
||||||
if (!p) { return; }
|
.filter(function (id) { return !!pos[id]; });
|
||||||
var ids = u.cards.map(function (c) { return String(c.id); });
|
if (!ids.length) { return; }
|
||||||
var yRow = pos[ids[0]].y; // 这一代卡片中心 y
|
var ys = ids.map(function (id) { return pos[id].y; });
|
||||||
var midX;
|
var y = ys[0]; // 同户必然同一行
|
||||||
if (ids.length >= 2) {
|
var cx = ids.map(function (id) { return pos[id].x; });
|
||||||
var a = pos[ids[0]], b = pos[ids[1]];
|
var left = Math.min.apply(null, cx), right = Math.max.apply(null, cx);
|
||||||
midX = (a.x + b.x) / 2;
|
anchor[ui] = {
|
||||||
// ① 夫妻横线:两卡中心之间的水平线(画在卡后,被卡面挡住)
|
gen: u.gen, y: y, yRow: y,
|
||||||
line(a.x, a.y, b.x, b.y);
|
left: left, right: right,
|
||||||
bars.push({ ui: ui, x1: a.x, x2: b.x, y: a.y, has: true, mid: midX });
|
mid: (left + right) / 2, // ★ 夫妻横线中点(单亲 = 卡中心)
|
||||||
} else {
|
top: y - IH / 2, bottom: y + IH / 2,
|
||||||
midX = pos[ids[0]].x;
|
hasBar: ids.length >= 2,
|
||||||
bars.push({ ui: ui, x1: midX, x2: midX, y: yRow, has: false, mid: midX });
|
ids: ids
|
||||||
}
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// ② 有子女 → 从夫妻横线**中点**往下垂到"横梁"
|
// 每一代的行 y(用于算"横梁"的位置:下一代卡顶再往上一点)
|
||||||
var kids = (kidsOf[ui] || []).filter(function (k) { return pos[units[k]._head] || true; });
|
var genY = {};
|
||||||
if (kids.length) {
|
Object.keys(anchor).forEach(function (ui) {
|
||||||
var busY = yRow + IH / 2 + SP / 2; // 参考:卡底 + SP/2
|
var a = anchor[ui];
|
||||||
line(midX, yRow, midX, busY);
|
if (genY[a.gen] === undefined) { genY[a.gen] = a.y; }
|
||||||
// ③ 横梁:从最左孩子到最右孩子
|
});
|
||||||
var kx = kids.map(function (k) { return placed[k].uc; });
|
function nextGenY(gen) {
|
||||||
var minK = Math.min.apply(null, kx), maxK = Math.max.apply(null, kx);
|
var best = null;
|
||||||
if (kids.length > 1 && maxK - minK > 0.5) { line(minK, busY, maxK, busY); }
|
Object.keys(genY).forEach(function (g) {
|
||||||
drops.push({ ui: ui, x: midX, y0: yRow, busY: busY });
|
var gg = +g;
|
||||||
}
|
if (gg > gen && (best === null || gg < best)) { best = gg; }
|
||||||
|
|
||||||
// ④ 每个人若有父母 → 从卡顶往上画一小段(接到上一代的横梁)
|
|
||||||
ids.forEach(function (id) {
|
|
||||||
var ps = parentsOf[id];
|
|
||||||
if (ps && ps.length) {
|
|
||||||
var pp = pos[id];
|
|
||||||
line(pp.x, pp.y - IH / 2, pp.x, pp.y - IH / 2 - SP / 2);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
return best === null ? null : genY[best];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- ③ 画线:只用 anchor 与 pos ----
|
||||||
|
// A. 夫妻横线
|
||||||
|
units.forEach(function (u, ui) {
|
||||||
|
var a = anchor[ui];
|
||||||
|
if (!a) { return; }
|
||||||
|
if (a.hasBar) { line(a.left, a.y, a.right, a.y); }
|
||||||
|
bars.push({ ui: ui, x1: a.left, x2: a.right, y: a.y, mid: a.mid, has: a.hasBar });
|
||||||
|
});
|
||||||
|
|
||||||
|
// B/C. 从"我"这一户往下连子女(横线中点 → 横梁 → 每个孩子的卡顶)
|
||||||
|
//
|
||||||
|
// ⚠️ 横梁 y 取「**子女那一代的卡片顶** 再往上 SP/2」——
|
||||||
|
// 上一版取的是"父户的 y + IH/2 + SP/2",当两代之间还有别的行时就会错位。
|
||||||
|
Object.keys(anchor).forEach(function (k) {
|
||||||
|
var ui = +k, a = anchor[ui];
|
||||||
|
var kids = (kidsOf[ui] || []).filter(function (kk) { return anchor[kk]; });
|
||||||
|
if (!kids.length) { return; }
|
||||||
|
// ⚠️⚠️ 横梁 y 必须由**孩子自己的卡顶**算,不能用"我这一代的下一代":
|
||||||
|
// 叔伯的子女与我同代(不是下一代),舅姨的子女也是同代 ——
|
||||||
|
// 用 nextGenY() 会把横梁画到别处,孩子的竖线于是接不上(实测 18 段悬空)。
|
||||||
|
var kidTops = kids.map(function (kk) { return anchor[kk].top; });
|
||||||
|
var busY = Math.min.apply(null, kidTops) - SP / 2; // 横梁:孩子卡顶再往上 SP/2
|
||||||
|
// 下垂线:从**卡底**(不是卡中心)→ 横梁,这样不会压在卡片身上
|
||||||
|
line(a.mid, a.bottom, a.mid, busY);
|
||||||
|
drops.push({ ui: ui, x: a.mid, y0: a.bottom, busY: busY });
|
||||||
|
|
||||||
|
// 横梁:从最左孩子连到最右孩子。
|
||||||
|
//
|
||||||
|
// ⚠️ 曾经担心"长横梁横穿全图",但那不是真正的问题 ——
|
||||||
|
// 横梁的 y 落在**两代之间的空隙**里,那里**没有任何卡片**,所以横梁不穿卡。
|
||||||
|
// 真正的问题是**布局**:同一对父母的子女被别家的孩子插在中间
|
||||||
|
// (实测"堂兄"夹在"哥哥"和"我"之间),横梁才被迫跨得很远。
|
||||||
|
// 那个已经在下面的 `groupSiblings()` 里修掉了(同父的子女会被排到一起)。
|
||||||
|
// 所以这里恢复成"最左 → 最右"的整条横梁,看起来才是一家人。
|
||||||
|
var ks = kids.map(function (kk) { return anchor[kk].mid; })
|
||||||
|
.sort(function (x, y2) { return x - y2; });
|
||||||
|
if (ks.length > 1) { line(ks[0], busY, ks[ks.length - 1], busY); }
|
||||||
|
|
||||||
|
// 每个孩子:从横梁往下接到**卡顶**
|
||||||
|
kids.forEach(function (kk) {
|
||||||
|
var c = anchor[kk];
|
||||||
|
line(c.mid, busY, c.mid, c.top);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// D. 每个"有父母"的人:**卡顶往上接到父母那一户的横梁**
|
||||||
|
//
|
||||||
|
// ⚠️ 这里必须用「**父母那一户**的下垂线终点 busY」,
|
||||||
|
// 而不是"本代卡顶 - SP/2" —— 后者只在"父母与我在相邻两代"时才相等,
|
||||||
|
// 姻亲/旁支一错位就接不上(实测 6 张卡"有父母却没有线连上去")。
|
||||||
|
// 上面 B/C 段已经把每户的 busY 算好放进 drops[],这里直接查。
|
||||||
|
var busOfUnit = {};
|
||||||
|
drops.forEach(function (d) { busOfUnit[d.ui] = d.busY; });
|
||||||
|
Object.keys(anchor).forEach(function (k) {
|
||||||
|
var ui = +k, a = anchor[ui];
|
||||||
|
var ps = parentsOf[String(ui)] || [];
|
||||||
|
if (!ps.length) { return; }
|
||||||
|
// 取"负责连他的那一户"(parentsOf 里的第一个)
|
||||||
|
var pu = ps[0];
|
||||||
|
var busY = busOfUnit[pu];
|
||||||
|
if (busY === undefined) {
|
||||||
|
// 父母那一户没有子女横梁(数据异常):退化到"本代卡顶 - SP/2"
|
||||||
|
busY = a.top - SP / 2;
|
||||||
|
}
|
||||||
|
line(a.mid, busY, a.mid, a.top);
|
||||||
});
|
});
|
||||||
|
|
||||||
root.insertAdjacentHTML('afterbegin',
|
root.insertAdjacentHTML('afterbegin',
|
||||||
|
|||||||
Reference in New Issue
Block a user