From 8cabc3540012dfd80871d8e45595bc2e5582a17e Mon Sep 17 00:00:00 2001 From: laoyang Date: Wed, 21 Oct 2026 22:57:56 +0800 Subject: [PATCH] =?UTF-8?q?v73b=EF=BC=9A=E5=AE=B6=E8=B0=B1=E8=BF=9E?= =?UTF-8?q?=E7=BA=BF=E6=94=B9=E6=88=90=E3=80=8C=E6=AF=8F=E5=AF=B9=E7=88=B6?= =?UTF-8?q?=E6=AF=8D=E4=B8=80=E6=9D=A1=E6=AF=8D=E7=BA=BF=E3=80=8D=EF=BC=8C?= =?UTF-8?q?=E5=8E=BB=E6=8E=89=E9=87=8D=E5=90=88=E7=9A=84=E6=A8=AA=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 旧画法每个子女各画一条独立折线,在真实数据上会画出两条横段完全 重合的线(岳父母→我户 216→72 与 我父母→姐户 72→216,拐点同为 132.9),视觉上是一个「口」字框,看着像父母家和岳父母家连在一起。 新画法:父母单元中点 ──竖──→ 母线高度 ──横(只覆盖自己子女)──→ 各子女 ──竖──→ 子女卡片上沿。一条线只服务一对父母,不再交叉重叠。 关键修正:子女挂点必须是**子女本人那张卡**,不是「子女所在户的中点」。 我这一户同时是两对父母的子女户(我父母→我、岳父母→妻子), 用户中点时两条竖线都落在 x=72、完全重合;改用本人卡的 x 后 妻子那条落在 105,两条线才分开。 ⚠️ 这类病端点断言完全抓不到(端点都好好贴在卡片上),所以测试侧 新增「没有任何两条线段完全重合」,并在视图侧说明 bus 段是同一折线 公式的退化情形、不必单独写分支。 验证:离线 30/0、反向 13/0、真实数据+屏幕对照 11/0、 像素级 9/0、真机几何 15/0、ArkTS 守卫 905/0。 --- entry/src/main/ets/common/KinLayout.ets | 216 ++++++++++++++++--- entry/src/main/ets/components/FbKinGraph.ets | 11 +- 2 files changed, 198 insertions(+), 29 deletions(-) diff --git a/entry/src/main/ets/common/KinLayout.ets b/entry/src/main/ets/common/KinLayout.ets index f037e6a..a54ca1a 100644 --- a/entry/src/main/ets/common/KinLayout.ets +++ b/entry/src/main/ets/common/KinLayout.ets @@ -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 = new Map(); + 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); + } } } diff --git a/entry/src/main/ets/components/FbKinGraph.ets b/entry/src/main/ets/components/FbKinGraph.ets index 1ad82a2..41cf88a 100644 --- a/entry/src/main/ets/components/FbKinGraph.ets +++ b/entry/src/main/ets/components/FbKinGraph.ets @@ -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;