v73b:家谱连线改成「每对父母一条母线」,去掉重合的横段
旧画法每个子女各画一条独立折线,在真实数据上会画出两条横段完全
重合的线(岳父母→我户 216→72 与 我父母→姐户 72→216,拐点同为
132.9),视觉上是一个「口」字框,看着像父母家和岳父母家连在一起。
新画法:父母单元中点 ──竖──→ 母线高度 ──横(只覆盖自己子女)──→
各子女 ──竖──→ 子女卡片上沿。一条线只服务一对父母,不再交叉重叠。
关键修正:子女挂点必须是**子女本人那张卡**,不是「子女所在户的中点」。
我这一户同时是两对父母的子女户(我父母→我、岳父母→妻子),
用户中点时两条竖线都落在 x=72、完全重合;改用本人卡的 x 后
妻子那条落在 105,两条线才分开。
⚠️ 这类病端点断言完全抓不到(端点都好好贴在卡片上),所以测试侧
新增「没有任何两条线段完全重合」,并在视图侧说明 bus 段是同一折线
公式的退化情形、不必单独写分支。
验证:离线 30/0、反向 13/0、真实数据+屏幕对照 11/0、
像素级 9/0、真机几何 15/0、ArkTS 守卫 905/0。
This commit is contained in:
@@ -89,17 +89,27 @@ export class KinCardPos {
|
||||
/**
|
||||
* 一条连线(Canvas 直接照着画,不需要再算)。
|
||||
*
|
||||
* 血缘线是**三段折线**:父母单元中点 → 往下 → 横移 → 往下 → 子女单元顶端。
|
||||
* 这里把拐点算好(`midY`),视图只负责 `moveTo/lineTo`。
|
||||
* 这里把拐点算好(`midY`),视图只负责 `moveTo/lineTo`:
|
||||
* `(x1,y1) → (x1,midY) → (x2,midY) → (x2,y2)`
|
||||
*
|
||||
* 🔴 v73b:画法从「每个子女一条独立折线」改成「**每对父母一条母线**」,
|
||||
* 于是 `blood` 的内涵变了 —— 它现在只画**竖直段**,横段单独是 `bus`:
|
||||
*
|
||||
* · `blood` 主竖线:父母单元中点 → 母线高度(`x1 === x2`,`y2 === midY`)
|
||||
* · `bus` 母线横段:只覆盖这对父母**自己子女**的跨度(`y1 === y2 === midY`)
|
||||
* · `blood` 子女短竖线:母线高度 → 子女卡片上沿(`x1 === x2`,`y1 === midY`)
|
||||
*
|
||||
* ⚠️ 三种都是上面那条折线公式的**退化情形**(要么横段长度为 0、要么竖段为 0),
|
||||
* 所以视图里**不需要为 `bus` 单独写分支**,同一段代码照样画对。
|
||||
*/
|
||||
export class KinEdge {
|
||||
/** 'blood' = 父母→子女;'spouse' = 夫妻 */
|
||||
/** 'blood' = 父母→子女的竖直段;'bus' = 母线横段;'spouse' = 夫妻 */
|
||||
kind: string = 'blood';
|
||||
x1: number = 0;
|
||||
y1: number = 0;
|
||||
x2: number = 0;
|
||||
y2: number = 0;
|
||||
/** 血缘线的横向拐弯 y(spouse 线不用) */
|
||||
/** 折线的拐弯 y(spouse 线不用;bus/blood 都等于母线高度) */
|
||||
midY: number = 0;
|
||||
}
|
||||
|
||||
@@ -619,23 +629,67 @@ function rowIndex(gens: number[], gen: number): number {
|
||||
}
|
||||
|
||||
/**
|
||||
* 血缘连线:从「父母单元」的中点垂到「子女单元」的顶端。
|
||||
* 血缘连线:**每对父母一条母线**。
|
||||
*
|
||||
* ⚠️ 起点用**父母单元中点**(夫妻横线的中点),不是父亲个人 ——
|
||||
* 这是 family-chart 的画法,也是「孩子是两个人共同的孩子」的视觉表达。
|
||||
* 原实现从父亲个人往下画,遇到母亲在右、孩子在中间时线会斜穿。
|
||||
* ============================================================
|
||||
* 🔴🔴 v73b:为什么不是「每个子女一条折线」(用户 2026-10-21:「连线还是乱七八糟的」)
|
||||
* ============================================================
|
||||
* 旧画法(v73 初版):每个子女各画一条 `父母中点 → 下 → 横移 → 下 → 子女` 的折线。
|
||||
* 病在真实数据上看得最清楚:
|
||||
*
|
||||
* 线A「岳父母(x=216) → 我这一户(x=72)」横段 216→72,拐点 y=132.9
|
||||
* 线B「我父母(x=72) → 姐这一户(x=216)」横段 72→216,拐点 y=132.9
|
||||
*
|
||||
* ⇒ 两条横段**完全重合**、拐点同高,画出来是一个「口」字框:
|
||||
*
|
||||
* 父母(72) 岳父母(216)
|
||||
* | |
|
||||
* +-----------------+ ← 两条线的横段叠在一起
|
||||
* | |
|
||||
* 我(72) 姐(216)
|
||||
*
|
||||
* 看上去就像"父母家和岳父母家连在一起、我和姐都挂在上面" —— 这正是用户说的
|
||||
* **「乱七八糟」**。而且 6 条线**全部**用同一个 0.42 拐点比例 ⇒ 所有横线挤在
|
||||
* 同一高度、糊成一片。
|
||||
*
|
||||
* ⚠️ 注意这类病**端点断言完全抓不到**(每个端点都好好地贴在卡片边缘上),
|
||||
* 所以离线套件里专门加了一条「没有任何两条线段完全重合」的断言
|
||||
* (见 `_fb_kin_layout_v73.py` ⑤b-2),否则"全绿但画面错"会一直重演。
|
||||
*
|
||||
* 新画法(家谱通行画法)——同一对父母的**所有子女共用一条母线**:
|
||||
*
|
||||
* 父母单元中点 ──竖──→ 母线高度 ──横(只覆盖自己子女跨度)──→ 各子女 ──竖──→ 子女顶端
|
||||
*
|
||||
* · 一条线只服务一对父母 ⇒ 不会再和别的父母的线交叉/重叠
|
||||
* · 横段只跨「自己子女」的范围,不会拉到别人头顶
|
||||
* · 每个子女只从母线上垂一根短竖线
|
||||
*/
|
||||
function buildBloodEdges(layout: KinLayout, people: FbKinPerson[],
|
||||
index: PeopleIndex, unitOfPerson: number[], units: KinUnit[],
|
||||
gens: number[]): void {
|
||||
const done: string[] = [];
|
||||
// ---- ① 先把「父母单元 → 它的子女(**人**)」聚起来 ----
|
||||
//
|
||||
// 🔴🔴 v73b 关键修正:挂点必须是**子女本人那张卡**,不是"子女所在户的中点"。
|
||||
//
|
||||
// 病根(真实数据实测):
|
||||
// 我这一户同时是**两对父母**的子女户 ——
|
||||
// · 我父母(户 -1:1)的子女:我的户、老姐的户
|
||||
// · 岳父母(户 -1:4)的子女:我的户 ← 因为妻子在那户里
|
||||
// 旧代码给"子女户"画竖线时用**户中点**(我这户 = 72)
|
||||
// ⇒ 两对父母的竖线**都落在 x=72**、y 区间也相同(都到下一行上沿)
|
||||
// ⇒ 画出来是两条**完全重合**的线,像"我父母和岳父母接到了同一个人"。
|
||||
// 正确语义:妻子是岳父母的女儿,竖线该落在**妻子那张卡**(x=105);
|
||||
// 我是我父母的儿子,竖线落在**我那张卡**(x=39)。
|
||||
//
|
||||
// ⇒ 按「人」记录:父母户 key → 子女本人 id 列表。挂点用该人的卡片 x,
|
||||
// 天然把同一户里的不同血缘分开。
|
||||
const kidsOf: Map<string, number[]> = new Map<string, number[]>();
|
||||
const parentKeys: string[] = []; // 保序(Map 迭代序在 ArkTS 里不可依赖)
|
||||
for (let i = 0; i < people.length; i++) {
|
||||
const child: FbKinPerson = people[i];
|
||||
const childCard: KinCardPos | undefined = cardOf(layout, child.id);
|
||||
if (childCard === undefined) {
|
||||
if (cardOf(layout, child.id) === undefined) {
|
||||
continue;
|
||||
}
|
||||
// ---- 每个子女只连一条线(父母可能有两个,共用一个"落点")----
|
||||
let pid: number = 0;
|
||||
for (let k = 0; k < child.parents.length; k++) {
|
||||
if (index.find(child.parents[k]) >= 0) {
|
||||
@@ -646,27 +700,133 @@ function buildBloodEdges(layout: KinLayout, people: FbKinPerson[],
|
||||
if (pid === 0) {
|
||||
continue;
|
||||
}
|
||||
const pu: number = unitOfPerson[index.find(pid)];
|
||||
const pi: number = index.find(pid);
|
||||
if (pi < 0) {
|
||||
continue;
|
||||
}
|
||||
const pu: number = unitOfPerson[pi];
|
||||
const cu: number = unitOfPerson[i];
|
||||
if (pu < 0 || cu < 0) {
|
||||
continue;
|
||||
}
|
||||
const pkey: string = units[pu].key;
|
||||
if (pkey === units[cu].key) {
|
||||
continue; // 同单元(夫妻互称父母)跳过
|
||||
}
|
||||
let list: number[] | undefined = kidsOf.get(pkey);
|
||||
if (list === undefined) {
|
||||
list = [];
|
||||
kidsOf.set(pkey, list);
|
||||
parentKeys.push(pkey);
|
||||
}
|
||||
if (list.indexOf(child.id) < 0) {
|
||||
list.push(child.id); // 去重(同一人可能登记了多个 parent)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- ② 每对父母画一组:主竖线 + 母线横段 + 各子女短竖线 ----
|
||||
for (let pi = 0; pi < parentKeys.length; pi++) {
|
||||
const pkey: string = parentKeys[pi];
|
||||
const pu: number = indexOfUnit(units, pkey);
|
||||
if (pu < 0) {
|
||||
continue;
|
||||
}
|
||||
const k2: string = units[pu].key + '>' + units[cu].key;
|
||||
if (done.indexOf(k2) >= 0) {
|
||||
continue; // 同一对「父母单元 → 子女单元」只画一次
|
||||
}
|
||||
done.push(k2);
|
||||
// 🔴 行号必须用 `rowIndex`(gens 下标),**不能**用 gen 数值 ——
|
||||
// 真机 bug「线没连对」就是这里(见 `rowIndex` 的注释)。
|
||||
const pgi: number = rowIndex(gens, units[pu].gen);
|
||||
const y1: number = pgi * KinDim.ROW_H + KinDim.CARD_H / 2;
|
||||
const px: number = units[pu].x;
|
||||
|
||||
const parentY: number = rowIndex(gens, units[pu].gen) * KinDim.ROW_H;
|
||||
const e: KinEdge = new KinEdge();
|
||||
e.kind = 'blood';
|
||||
e.x1 = units[pu].x;
|
||||
e.y1 = parentY + KinDim.CARD_H / 2;
|
||||
e.x2 = units[cu].x;
|
||||
e.y2 = rowIndex(gens, units[cu].gen) * KinDim.ROW_H - KinDim.CARD_H / 2;
|
||||
// 拐点在两代之间靠上 40% 处:靠近父母的下沿,视觉上「分叉」更像扇出
|
||||
e.midY = e.y1 + (e.y2 - e.y1) * 0.42;
|
||||
layout.edges.push(e);
|
||||
// 子女只保留在**下一代**的:脏数据可能把同代/上一代也登记成子女
|
||||
const raw: number[] | undefined = kidsOf.get(pkey);
|
||||
if (raw === undefined) {
|
||||
continue;
|
||||
}
|
||||
const kidXs: number[] = [];
|
||||
const kidIds: number[] = [];
|
||||
for (let k = 0; k < raw.length; k++) {
|
||||
const kid: number = raw[k];
|
||||
const kidCard: KinCardPos | undefined = cardOf(layout, kid);
|
||||
if (kidCard === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (rowIndex(gens, kidCard.p.gen) <= pgi) {
|
||||
continue;
|
||||
}
|
||||
kidIds.push(kid);
|
||||
kidXs.push(kidCard.x);
|
||||
}
|
||||
if (kidIds.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 按卡片 x 排序(母线从左往右画,不来回折)
|
||||
const order: number[] = [];
|
||||
for (let k = 0; k < kidIds.length; k++) {
|
||||
order.push(k);
|
||||
}
|
||||
order.sort((a: number, b: number): number => kidXs[a] - kidXs[b]);
|
||||
|
||||
let left: number = 1e9;
|
||||
let right: number = -1e9;
|
||||
let nearestRow: number = 1e9;
|
||||
for (let k = 0; k < order.length; k++) {
|
||||
const x: number = kidXs[order[k]];
|
||||
if (x < left) {
|
||||
left = x;
|
||||
}
|
||||
if (x > right) {
|
||||
right = x;
|
||||
}
|
||||
const kidCard2: KinCardPos | undefined = cardOf(layout, kidIds[order[k]]);
|
||||
if (kidCard2 !== undefined) {
|
||||
const r: number = rowIndex(gens, kidCard2.p.gen);
|
||||
if (r < nearestRow) {
|
||||
nearestRow = r;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 母线高度:落在父母下沿与**最近那代子女**上沿之间,靠子女一侧(分叉更明显)
|
||||
const yChildTop: number = nearestRow * KinDim.ROW_H - KinDim.CARD_H / 2;
|
||||
const busY: number = y1 + (yChildTop - y1) * 0.62;
|
||||
|
||||
// 主竖线:父母单元中点 → 母线高度
|
||||
const stem: KinEdge = new KinEdge();
|
||||
stem.kind = 'blood';
|
||||
stem.x1 = px;
|
||||
stem.y1 = y1;
|
||||
stem.x2 = px;
|
||||
stem.y2 = busY;
|
||||
stem.midY = busY;
|
||||
layout.edges.push(stem);
|
||||
|
||||
// 母线横段:只有一个子女时省掉(否则是零长线,白占一条)
|
||||
if (right - left > 0.5) {
|
||||
const bus: KinEdge = new KinEdge();
|
||||
bus.kind = 'bus';
|
||||
bus.x1 = left;
|
||||
bus.y1 = busY;
|
||||
bus.x2 = right;
|
||||
bus.y2 = busY;
|
||||
bus.midY = busY;
|
||||
layout.edges.push(bus);
|
||||
}
|
||||
|
||||
// 各子女:从母线上垂一根短竖线到**自己那张卡**的上沿
|
||||
for (let k = 0; k < order.length; k++) {
|
||||
const kidCard3: KinCardPos | undefined = cardOf(layout, kidIds[order[k]]);
|
||||
if (kidCard3 === undefined) {
|
||||
continue;
|
||||
}
|
||||
const drop: KinEdge = new KinEdge();
|
||||
drop.kind = 'blood';
|
||||
drop.x1 = kidCard3.x;
|
||||
drop.y1 = busY;
|
||||
drop.x2 = kidCard3.x;
|
||||
drop.y2 = rowIndex(gens, kidCard3.p.gen) * KinDim.ROW_H - KinDim.CARD_H / 2;
|
||||
drop.midY = busY;
|
||||
layout.edges.push(drop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -210,7 +210,16 @@ export struct FbKinGraphView {
|
||||
const c: CanvasRenderingContext2D = this.ctx;
|
||||
c.clearRect(0, 0, this.contentW, this.contentH);
|
||||
|
||||
// ---- 血缘:父母单元中点 → 下 → 横 → 下 → 子女顶端(与 Web 端同款折线)----
|
||||
// ---- 血缘:`(x1,y1) → (x1,midY) → (x2,midY) → (x2,y2)` ----
|
||||
//
|
||||
// 🔴 v73b:这里**不需要**为母线(`bus`)写分支 —— 母线横段只是同一个公式的
|
||||
// 退化情形(`y1 === y2 === midY` ⇒ 头和尾那两段竖线长度为 0),
|
||||
// 照这个公式画出来正好是一条纯横线。三类几何都画对:
|
||||
// · 主竖线 x1===x2 ⇒ 纯竖线
|
||||
// · 母线横段 y1===y2===midY ⇒ 纯横线
|
||||
// · 子女短竖线 x1===x2 ⇒ 纯竖线
|
||||
// ⚠️ 别为了"看得更清楚"给 bus 单独写一段:两段代码会各自漂移,
|
||||
// 以后改拐点或线宽就会不一致。
|
||||
for (const e of this.layout.edges) {
|
||||
if (e.kind === 'spouse') {
|
||||
continue;
|
||||
|
||||
Reference in New Issue
Block a user